From c99a96a8c8ca9ed2ff1efcf7c353bfd4db0d8cc9 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Tue, 17 Aug 2021 13:39:18 +0000 Subject: [PATCH 001/391] initial rhythm calc testing --- .../Difficulty/Skills/Aim.cs | 30 +++- .../Difficulty/Skills/OsuStrainSkill.cs | 2 +- .../Difficulty/Skills/Speed.cs | 133 +++++++++++++++++- 3 files changed, 151 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 16a18cbcb9..776cca8657 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -22,17 +22,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { } - protected override double SkillMultiplier => 26.25; - protected override double StrainDecayBase => 0.15; + private double currentStrain = 1; - protected override double StrainValueOf(DifficultyHitObject current) + private double skillMultiplier => 26.25; + private double strainDecayBase => 0.15; + + private double aimStrainOf(DifficultyHitObject current) { if (current.BaseObject is Spinner) return 0; var osuCurrent = (OsuDifficultyHitObject)current; - double result = 0; + double aimStrain = 0; if (Previous.Count > 0) { @@ -46,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills Math.Max(osuPrevious.JumpDistance - scale, 0) * Math.Pow(Math.Sin(osuCurrent.Angle.Value - angle_bonus_begin), 2) * Math.Max(osuCurrent.JumpDistance - scale, 0)); - result = 1.4 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime); + aimStrain = 1.4 * applyDiminishingExp(Math.Max(0, angleBonus)) / Math.Max(timing_threshold, osuPrevious.StrainTime); } } @@ -54,11 +56,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills double travelDistanceExp = applyDiminishingExp(osuCurrent.TravelDistance); return Math.Max( - result + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(osuCurrent.StrainTime, timing_threshold), + aimStrain + (jumpDistanceExp + travelDistanceExp + Math.Sqrt(travelDistanceExp * jumpDistanceExp)) / Math.Max(osuCurrent.StrainTime, timing_threshold), (Math.Sqrt(travelDistanceExp * jumpDistanceExp) + jumpDistanceExp + travelDistanceExp) / osuCurrent.StrainTime ); } private double applyDiminishingExp(double val) => Math.Pow(val, 0.99); + + private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); + + protected override double CalculateInitialStrain(double time) => currentStrain * strainDecay(time - Previous[0].StartTime); + + protected override double StrainValueAt(DifficultyHitObject current) + { + double aimStrain = aimStrainOf(current); + + currentStrain *= strainDecay(current.DeltaTime); + currentStrain += aimStrain * skillMultiplier; + +// Console.WriteLine(currentStrain); + + return currentStrain; + } } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs index 7bcd867a9c..e47edc37cc 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/OsuStrainSkill.cs @@ -10,7 +10,7 @@ using osu.Framework.Utils; namespace osu.Game.Rulesets.Osu.Difficulty.Skills { - public abstract class OsuStrainSkill : StrainDecaySkill + public abstract class OsuStrainSkill : StrainSkill { /// /// The number of sections with the highest strains, which the peak strain reductions will apply to. diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index f0eb199e5f..314bb31465 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -20,24 +20,121 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private const double pi_over_4 = Math.PI / 4; private const double pi_over_2 = Math.PI / 2; - protected override double SkillMultiplier => 1400; - protected override double StrainDecayBase => 0.3; + private const double rhythmMultiplier = 1.5; + + private double skillMultiplier => 1400; + private double strainDecayBase => 0.3; + private double difficultyMultiplier => 1.04; + + private double currentTapStrain = 1; + private double currentMovementStrain = 1; + protected override int ReducedSectionCount => 5; - protected override double DifficultyMultiplier => 1.04; private const double min_speed_bonus = 75; // ~200BPM private const double max_speed_bonus = 45; // ~330BPM private const double speed_balancing_factor = 40; + protected override int HistoryLength => 32; + + private const int HistoryTimeMax = 3000; // 4 seconds of calculatingRhythmBonus max. + public Speed(Mod[] mods) : base(mods) { } - protected override double StrainValueOf(DifficultyHitObject current) + private bool isRatioEqual(double ratio, double a, double b) { - if (current.BaseObject is Spinner) - return 0; + return a + 15 > ratio * b && a - 15 < ratio * b; + } + + /// + /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . + /// + private double calculateRhythmBonus(double startTime) + { + // {doubles, triplets, quads, quints, 6-tuplets, 7 Tuplets, greater} + int previousIslandSize = -1; + double[] islandTimes = {0, 0, 0, 0, 0, 0, 0}; + int islandSize = 0; + + bool firstDeltaSwitch = false; + + for (int i = Previous.Count - 1; i > 0; i--) + { + double currDelta = ((OsuDifficultyHitObject)Previous[i - 1]).StrainTime; + double prevDelta = ((OsuDifficultyHitObject)Previous[i]).StrainTime; + double effectiveRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta); + + effectiveRatio *= Math.Sqrt(100 / ((currDelta + prevDelta) / 2)); // scale with bpm. + + if (effectiveRatio > 0.5) + effectiveRatio = 0.5 + (effectiveRatio - 0.5) * 5; // extra buff for 1/3 -> 1/4 etc transitions. + + double currHistoricalDecay = Math.Max(0, (HistoryTimeMax - (startTime - Previous[i - 1].StartTime))) / HistoryTimeMax; + + if (firstDeltaSwitch) + { + if (isRatioEqual(1.0, prevDelta, currDelta)) + { + islandSize++; // island is still progressing, count size. + } + + else + { + if (islandSize > 6) + islandSize = 6; + + if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window + effectiveRatio *= 0.5; + + if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle + effectiveRatio *= 0.75; + + if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet) + effectiveRatio *= 0.5; + + islandTimes[islandSize] = islandTimes[islandSize] + effectiveRatio * currHistoricalDecay; + + previousIslandSize = islandSize; // log the last island size. + + if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting + firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size. + + islandSize = 0; + } + } + else if (prevDelta > 1.25 * currDelta) // we want to be speeding up. + { + // Begin counting island until we change speed again. + firstDeltaSwitch = true; + islandSize = 0; + } + } + + double rhythmComplexitySum = 0.0; + + for (int i = 0; i < islandTimes.Length; i++) + { + rhythmComplexitySum += islandTimes[i]; // sum the total amount of rhythm variance + } + +// Console.WriteLine(Math.Sqrt(4 + rhythmComplexitySum * rhythmMultiplier) / 2); + + return Math.Sqrt(4 + rhythmComplexitySum * rhythmMultiplier) / 2; + } + + private void setStrainValues(DifficultyHitObject current) + { + // if (current.BaseObject is Spinner) + // { + // currentTapStrain *= strainDecay(current.DeltaTime); + // + // currentMovementStrain *= strainDecay(current.DeltaTime); + // + // return; + // } var osuCurrent = (OsuDifficultyHitObject)current; @@ -64,7 +161,29 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills } } - return (1 + (speedBonus - 1) * 0.75) * angleBonus * (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / osuCurrent.StrainTime; + // return (1 + (speedBonus - 1) * 0.75) * angleBonus * (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / osuCurrent.StrainTime; + + double tapStrain = ((1 + (speedBonus - 1) * 0.75) * 0.95) / osuCurrent.StrainTime; + double movementStrain = ((1 + (speedBonus - 1) * 0.75) * angleBonus * speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / osuCurrent.StrainTime; + + currentTapStrain *= strainDecay(current.DeltaTime); + currentTapStrain += tapStrain * skillMultiplier; + + currentMovementStrain *= strainDecay(current.DeltaTime); + currentMovementStrain += movementStrain * skillMultiplier; + } + + private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); + + protected override double CalculateInitialStrain(double time) => (currentMovementStrain + currentTapStrain) * strainDecay(time - Previous[0].StartTime); + + protected override double StrainValueAt(DifficultyHitObject current) + { + setStrainValues(current); + +// Console.WriteLine(currentMovementStrain + currentTapStrain); + + return currentMovementStrain + currentTapStrain * calculateRhythmBonus(current.StartTime); } } } From 471ae9664e67fd164276ce07db3700ce2296631b Mon Sep 17 00:00:00 2001 From: Xexxar Date: Tue, 17 Aug 2021 13:47:45 +0000 Subject: [PATCH 002/391] cleaned up jank --- .../Difficulty/Skills/Speed.cs | 62 +++++++++++++------ 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 314bb31465..89dd7efbdf 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -125,16 +125,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills return Math.Sqrt(4 + rhythmComplexitySum * rhythmMultiplier) / 2; } - private void setStrainValues(DifficultyHitObject current) + private double tapStrainOf(DifficultyHitObject current) { - // if (current.BaseObject is Spinner) - // { - // currentTapStrain *= strainDecay(current.DeltaTime); - // - // currentMovementStrain *= strainDecay(current.DeltaTime); - // - // return; - // } + if (current.BaseObject is Spinner) + { + return 0; + } var osuCurrent = (OsuDifficultyHitObject)current; @@ -161,16 +157,42 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills } } - // return (1 + (speedBonus - 1) * 0.75) * angleBonus * (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / osuCurrent.StrainTime; + return ((1 + (speedBonus - 1) * 0.75) * angleBonus * 0.95) / osuCurrent.StrainTime; + } - double tapStrain = ((1 + (speedBonus - 1) * 0.75) * 0.95) / osuCurrent.StrainTime; - double movementStrain = ((1 + (speedBonus - 1) * 0.75) * angleBonus * speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / osuCurrent.StrainTime; + private double movementStrainOf(DifficultyHitObject current) + { + if (current.BaseObject is Spinner) + { + return 0; + } - currentTapStrain *= strainDecay(current.DeltaTime); - currentTapStrain += tapStrain * skillMultiplier; + var osuCurrent = (OsuDifficultyHitObject)current; - currentMovementStrain *= strainDecay(current.DeltaTime); - currentMovementStrain += movementStrain * skillMultiplier; + double distance = Math.Min(single_spacing_threshold, osuCurrent.TravelDistance + osuCurrent.JumpDistance); + double deltaTime = Math.Max(max_speed_bonus, current.DeltaTime); + + double speedBonus = 1.0; + if (deltaTime < min_speed_bonus) + speedBonus = 1 + Math.Pow((min_speed_bonus - deltaTime) / speed_balancing_factor, 2); + + double angleBonus = 1.0; + + if (osuCurrent.Angle != null && osuCurrent.Angle.Value < angle_bonus_begin) + { + angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - osuCurrent.Angle.Value)), 2) / 3.57; + + if (osuCurrent.Angle.Value < pi_over_2) + { + angleBonus = 1.28; + if (distance < 90 && osuCurrent.Angle.Value < pi_over_4) + angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1); + else if (distance < 90) + angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1) * Math.Sin((pi_over_2 - osuCurrent.Angle.Value) / pi_over_4); + } + } + + return ((1 + (speedBonus - 1) * 0.75) * angleBonus * speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / osuCurrent.StrainTime; } private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); @@ -179,11 +201,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills protected override double StrainValueAt(DifficultyHitObject current) { - setStrainValues(current); + currentTapStrain *= strainDecay(current.DeltaTime); + currentTapStrain += tapStrainOf(current) * skillMultiplier; -// Console.WriteLine(currentMovementStrain + currentTapStrain); + currentMovementStrain *= strainDecay(current.DeltaTime); + currentMovementStrain += movementStrainOf(current) * skillMultiplier; - return currentMovementStrain + currentTapStrain * calculateRhythmBonus(current.StartTime); + return currentMovementStrain + currentTapStrain;// * calculateRhythmBonus(current.StartTime); } } } From ac1ed00f3e0847619bbe11e37fc1f0260924dbf3 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Tue, 17 Aug 2021 14:39:43 +0000 Subject: [PATCH 003/391] added initial balance of rhythm complexity to tap --- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 89dd7efbdf..52fc77b6f6 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -24,12 +24,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double skillMultiplier => 1400; private double strainDecayBase => 0.3; - private double difficultyMultiplier => 1.04; private double currentTapStrain = 1; private double currentMovementStrain = 1; protected override int ReducedSectionCount => 5; + protected override double DifficultyMultiplier => 1.04; private const double min_speed_bonus = 75; // ~200BPM private const double max_speed_bonus = 45; // ~330BPM @@ -67,10 +67,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills double prevDelta = ((OsuDifficultyHitObject)Previous[i]).StrainTime; double effectiveRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta); - effectiveRatio *= Math.Sqrt(100 / ((currDelta + prevDelta) / 2)); // scale with bpm. + effectiveRatio *= Math.Sqrt(100 / ((currDelta + prevDelta) / 2)); // scale with bpm slightly if (effectiveRatio > 0.5) - effectiveRatio = 0.5 + (effectiveRatio - 0.5) * 5; // extra buff for 1/3 -> 1/4 etc transitions. + effectiveRatio = 0.5 + (effectiveRatio - 0.5) * 10; // extra buff for 1/3 -> 1/4 etc transitions. double currHistoricalDecay = Math.Max(0, (HistoryTimeMax - (startTime - Previous[i - 1].StartTime))) / HistoryTimeMax; @@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills for (int i = 0; i < islandTimes.Length; i++) { - rhythmComplexitySum += islandTimes[i]; // sum the total amount of rhythm variance + rhythmComplexitySum += islandTimes[i] * ((double)(i + islandTimes.Length) / (2 * islandTimes.Length)); // sum the total amount of rhythm variance } // Console.WriteLine(Math.Sqrt(4 + rhythmComplexitySum * rhythmMultiplier) / 2); @@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills } } - return ((1 + (speedBonus - 1) * 0.75) * angleBonus * 0.95) / osuCurrent.StrainTime; + return ((1 + (speedBonus - 1) * 0.75) * 0.95) / osuCurrent.StrainTime; } private double movementStrainOf(DifficultyHitObject current) @@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); - protected override double CalculateInitialStrain(double time) => (currentMovementStrain + currentTapStrain) * strainDecay(time - Previous[0].StartTime); + protected override double CalculateInitialStrain(double time) => (currentMovementStrain + currentTapStrain * calculateRhythmBonus(time)) * strainDecay(time - Previous[0].StartTime); protected override double StrainValueAt(DifficultyHitObject current) { @@ -207,7 +207,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills currentMovementStrain *= strainDecay(current.DeltaTime); currentMovementStrain += movementStrainOf(current) * skillMultiplier; - return currentMovementStrain + currentTapStrain;// * calculateRhythmBonus(current.StartTime); + return currentMovementStrain + currentTapStrain * calculateRhythmBonus(current.StartTime); } } } From b9ba4c1d9761e8eb5c8556f592303961033cd83d Mon Sep 17 00:00:00 2001 From: Xexxar Date: Tue, 17 Aug 2021 19:25:49 +0000 Subject: [PATCH 004/391] finalized change for PR --- .../Difficulty/Skills/Speed.cs | 63 +++++++------------ 1 file changed, 21 insertions(+), 42 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 52fc77b6f6..935e80ebc5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -21,12 +21,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private const double pi_over_2 = Math.PI / 2; private const double rhythmMultiplier = 1.5; + private const int HistoryTimeMax = 3000; // 3 seconds of calculatingRhythmBonus max. private double skillMultiplier => 1400; private double strainDecayBase => 0.3; private double currentTapStrain = 1; private double currentMovementStrain = 1; + private double currentRhythm = 1; protected override int ReducedSectionCount => 5; protected override double DifficultyMultiplier => 1.04; @@ -37,8 +39,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills protected override int HistoryLength => 32; - private const int HistoryTimeMax = 3000; // 4 seconds of calculatingRhythmBonus max. - public Speed(Mod[] mods) : base(mods) { @@ -52,8 +52,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills /// /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . /// - private double calculateRhythmBonus(double startTime) + private double calculateRhythmBonus(DifficultyHitObject current) { + if (current.BaseObject is Spinner) + return 0; + // {doubles, triplets, quads, quints, 6-tuplets, 7 Tuplets, greater} int previousIslandSize = -1; double[] islandTimes = {0, 0, 0, 0, 0, 0, 0}; @@ -72,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (effectiveRatio > 0.5) effectiveRatio = 0.5 + (effectiveRatio - 0.5) * 10; // extra buff for 1/3 -> 1/4 etc transitions. - double currHistoricalDecay = Math.Max(0, (HistoryTimeMax - (startTime - Previous[i - 1].StartTime))) / HistoryTimeMax; + double currHistoricalDecay = Math.Max(0, (HistoryTimeMax - (current.StartTime - Previous[i - 1].StartTime))) / HistoryTimeMax; if (firstDeltaSwitch) { @@ -125,56 +128,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills return Math.Sqrt(4 + rhythmComplexitySum * rhythmMultiplier) / 2; } - private double tapStrainOf(DifficultyHitObject current) + private double tapStrainOf(DifficultyHitObject current, double speedBonus) { if (current.BaseObject is Spinner) - { return 0; - } var osuCurrent = (OsuDifficultyHitObject)current; - double distance = Math.Min(single_spacing_threshold, osuCurrent.TravelDistance + osuCurrent.JumpDistance); - double deltaTime = Math.Max(max_speed_bonus, current.DeltaTime); - - double speedBonus = 1.0; - if (deltaTime < min_speed_bonus) - speedBonus = 1 + Math.Pow((min_speed_bonus - deltaTime) / speed_balancing_factor, 2); - - double angleBonus = 1.0; - - if (osuCurrent.Angle != null && osuCurrent.Angle.Value < angle_bonus_begin) - { - angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - osuCurrent.Angle.Value)), 2) / 3.57; - - if (osuCurrent.Angle.Value < pi_over_2) - { - angleBonus = 1.28; - if (distance < 90 && osuCurrent.Angle.Value < pi_over_4) - angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1); - else if (distance < 90) - angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1) * Math.Sin((pi_over_2 - osuCurrent.Angle.Value) / pi_over_4); - } - } - return ((1 + (speedBonus - 1) * 0.75) * 0.95) / osuCurrent.StrainTime; } - private double movementStrainOf(DifficultyHitObject current) + private double movementStrainOf(DifficultyHitObject current, double speedBonus) { if (current.BaseObject is Spinner) - { return 0; - } var osuCurrent = (OsuDifficultyHitObject)current; double distance = Math.Min(single_spacing_threshold, osuCurrent.TravelDistance + osuCurrent.JumpDistance); - double deltaTime = Math.Max(max_speed_bonus, current.DeltaTime); - - double speedBonus = 1.0; - if (deltaTime < min_speed_bonus) - speedBonus = 1 + Math.Pow((min_speed_bonus - deltaTime) / speed_balancing_factor, 2); double angleBonus = 1.0; @@ -197,17 +168,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); - protected override double CalculateInitialStrain(double time) => (currentMovementStrain + currentTapStrain * calculateRhythmBonus(time)) * strainDecay(time - Previous[0].StartTime); + protected override double CalculateInitialStrain(double time) => (currentMovementStrain + currentTapStrain * currentRhythm) * strainDecay(time - Previous[0].StartTime); protected override double StrainValueAt(DifficultyHitObject current) { + double speedBonus = 1.0; + double deltaTime = Math.Max(max_speed_bonus, current.DeltaTime); + + if (deltaTime < min_speed_bonus) + speedBonus = 1 + Math.Pow((min_speed_bonus - deltaTime) / speed_balancing_factor, 2); + + currentRhythm = calculateRhythmBonus(current); + currentTapStrain *= strainDecay(current.DeltaTime); - currentTapStrain += tapStrainOf(current) * skillMultiplier; + currentTapStrain += tapStrainOf(current, speedBonus) * skillMultiplier; currentMovementStrain *= strainDecay(current.DeltaTime); - currentMovementStrain += movementStrainOf(current) * skillMultiplier; + currentMovementStrain += movementStrainOf(current, speedBonus) * skillMultiplier; - return currentMovementStrain + currentTapStrain * calculateRhythmBonus(current.StartTime); + return currentMovementStrain + currentTapStrain * currentRhythm; } } } From 0effc8f5d892910964063ac096a303cf5cbb495a Mon Sep 17 00:00:00 2001 From: Xexxar Date: Thu, 19 Aug 2021 14:12:03 +0000 Subject: [PATCH 005/391] refactored speed skill, implemented better acc pp --- .../Difficulty/OsuPerformanceCalculator.cs | 36 +++-- .../Difficulty/Skills/Aim.cs | 2 - .../Difficulty/Skills/Speed.cs | 132 ++++++++---------- 3 files changed, 84 insertions(+), 86 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index e6ab978dfb..42f65ce4ed 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -161,6 +161,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Scale the speed value with accuracy and OD speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2); + // Punish low OD severely prevent OD abuse on rhythmically complex songs. + speedValue *= Math.Max(0.75, (Math.Min(8, Attributes.OverallDifficulty) / 8)); // Scale the speed value with # of 50s to punish doubletapping. speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); @@ -169,25 +171,31 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeAccuracyValue() { - // 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 (amountHitObjectsWithAccuracy > 0) - betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); - else - betterAccuracyPercentage = 0; + // This section should be documented by Tr3, but effectively we're calculating the exact same way as before, but + // we calculate a variance based on the object count and # of 50s, 100s, etc. This prevents us from having cases + // where an SS on lower OD is actually worth more than a 95% on OD11, even though the OD11 requires a greater + // window of precision. - // It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points - if (betterAccuracyPercentage < 0) - betterAccuracyPercentage = 0; + double p100 = (2 * (double)countOk) / amountHitObjectsWithAccuracy; // this is multiplied by two to encourage better accuracy. (scales better) + double p50 = (1 * (double)countMeh) / amountHitObjectsWithAccuracy; + double pm = (1 * (double)countMiss) / amountHitObjectsWithAccuracy; + double p300 = 1.0 - pm - p100 - p50; - // Lots of arbitrary values from testing. - // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution - double accuracyValue = Math.Pow(1.52163, Attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83; + double m300 = 79.5 - 6.0 * Attributes.OverallDifficulty; + double m100 = 139.5 - 8.0 * Attributes.OverallDifficulty; + double m50 = 199.5 - 10.0 * Attributes.OverallDifficulty; + double acc = p300 + 1.0 / 3.0 * p100 + 1.0 / 6.0 * p50; + + double variance = p300 * Math.Pow(m300 / 2.0, 2.0) + + p100 * Math.Pow((m300 + m100) / 2.0, 2.0) + + p50 * Math.Pow((m100 + m50) / 2.0, 2.0) + + pm * Math.Pow(229.5 - 11 * Attributes.OverallDifficulty, 2.0); + + double accuracyValue = 2.83 * Math.Pow(1.52163, (79.5 - 2 * Math.Sqrt(variance)) / 6.0) + * Math.Pow(Math.Log(1.0 + (Math.E - 1.0) * (Math.Min(amountHitObjectsWithAccuracy, 1600) / 1000.0)), 0.5); - // Bonus for many hitcircles - it's harder to keep good accuracy up for longer - accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3)); if (mods.Any(m => m is OsuModHidden)) accuracyValue *= 1.08; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 776cca8657..93df6e90f8 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -74,8 +74,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills currentStrain *= strainDecay(current.DeltaTime); currentStrain += aimStrain * skillMultiplier; -// Console.WriteLine(currentStrain); - return currentStrain; } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 935e80ebc5..b29e8be1a2 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -20,10 +20,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private const double pi_over_4 = Math.PI / 4; private const double pi_over_2 = Math.PI / 2; - private const double rhythmMultiplier = 1.5; + private const double rhythmMultiplier = 2.5; private const int HistoryTimeMax = 3000; // 3 seconds of calculatingRhythmBonus max. - private double skillMultiplier => 1400; + private double skillMultiplier => 1375; private double strainDecayBase => 0.3; private double currentTapStrain = 1; @@ -57,75 +57,70 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (current.BaseObject is Spinner) return 0; - // {doubles, triplets, quads, quints, 6-tuplets, 7 Tuplets, greater} int previousIslandSize = -1; - double[] islandTimes = {0, 0, 0, 0, 0, 0, 0}; + double rhythmComplexitySum = 0; int islandSize = 0; bool firstDeltaSwitch = false; for (int i = Previous.Count - 1; i > 0; i--) { - double currDelta = ((OsuDifficultyHitObject)Previous[i - 1]).StrainTime; - double prevDelta = ((OsuDifficultyHitObject)Previous[i]).StrainTime; - double effectiveRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta); + double currHistoricalDecay = Math.Max(0, (HistoryTimeMax - (current.StartTime - Previous[i - 1].StartTime))) / HistoryTimeMax; // scales note 0 to 1 from history to now - effectiveRatio *= Math.Sqrt(100 / ((currDelta + prevDelta) / 2)); // scale with bpm slightly - - if (effectiveRatio > 0.5) - effectiveRatio = 0.5 + (effectiveRatio - 0.5) * 10; // extra buff for 1/3 -> 1/4 etc transitions. - - double currHistoricalDecay = Math.Max(0, (HistoryTimeMax - (current.StartTime - Previous[i - 1].StartTime))) / HistoryTimeMax; - - if (firstDeltaSwitch) + if (currHistoricalDecay != 0) { - if (isRatioEqual(1.0, prevDelta, currDelta)) + currHistoricalDecay = Math.Max(currHistoricalDecay, (Previous.Count - i) / Previous.Count); // either we're limited by time or limited by object count. + + double currDelta = ((OsuDifficultyHitObject)Previous[i - 1]).StrainTime; + double prevDelta = ((OsuDifficultyHitObject)Previous[i]).StrainTime; + double effectiveRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta); + + if (effectiveRatio > 0.5) + effectiveRatio = 0.5 + (effectiveRatio - 0.5) * 10; // large buff for 1/3 -> 1/4 type transitions. + + effectiveRatio *= Math.Sqrt(100 / ((currDelta + prevDelta) / 2)) * currHistoricalDecay; // scale with bpm slightly and with time + + if (firstDeltaSwitch) { - islandSize++; // island is still progressing, count size. + if (isRatioEqual(1.0, prevDelta, currDelta)) + { + islandSize++; // island is still progressing, count size. + } + + else + { + if (islandSize > 6) + islandSize = 6; + + if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window + effectiveRatio *= 0.25; + + if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle + effectiveRatio *= 0.5; + + if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet) + effectiveRatio *= 0.25; + + rhythmComplexitySum += effectiveRatio * currHistoricalDecay; + + previousIslandSize = islandSize; // log the last island size. + + if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting + firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size. + + islandSize = 0; + } } - - else + else if (prevDelta > 1.25 * currDelta) // we want to be speeding up. { - if (islandSize > 6) - islandSize = 6; - - if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window - effectiveRatio *= 0.5; - - if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle - effectiveRatio *= 0.75; - - if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet) - effectiveRatio *= 0.5; - - islandTimes[islandSize] = islandTimes[islandSize] + effectiveRatio * currHistoricalDecay; - - previousIslandSize = islandSize; // log the last island size. - - if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting - firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size. - + // Begin counting island until we change speed again. + firstDeltaSwitch = true; islandSize = 0; } } - else if (prevDelta > 1.25 * currDelta) // we want to be speeding up. - { - // Begin counting island until we change speed again. - firstDeltaSwitch = true; - islandSize = 0; - } } - double rhythmComplexitySum = 0.0; - - for (int i = 0; i < islandTimes.Length; i++) - { - rhythmComplexitySum += islandTimes[i] * ((double)(i + islandTimes.Length) / (2 * islandTimes.Length)); // sum the total amount of rhythm variance - } - -// Console.WriteLine(Math.Sqrt(4 + rhythmComplexitySum * rhythmMultiplier) / 2); - - return Math.Sqrt(4 + rhythmComplexitySum * rhythmMultiplier) / 2; + return Math.Sqrt(4 + rhythmComplexitySum * rhythmMultiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) } private double tapStrainOf(DifficultyHitObject current, double speedBonus) @@ -133,9 +128,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (current.BaseObject is Spinner) return 0; - var osuCurrent = (OsuDifficultyHitObject)current; + var osuCurrObj = (OsuDifficultyHitObject)current; - return ((1 + (speedBonus - 1) * 0.75) * 0.95) / osuCurrent.StrainTime; + return speedBonus / osuCurrObj.StrainTime; } private double movementStrainOf(DifficultyHitObject current, double speedBonus) @@ -143,27 +138,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (current.BaseObject is Spinner) return 0; - var osuCurrent = (OsuDifficultyHitObject)current; + var osuCurrObj = (OsuDifficultyHitObject)current; - double distance = Math.Min(single_spacing_threshold, osuCurrent.TravelDistance + osuCurrent.JumpDistance); + double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.JumpDistance); double angleBonus = 1.0; - if (osuCurrent.Angle != null && osuCurrent.Angle.Value < angle_bonus_begin) + if (osuCurrObj.Angle != null) { - angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - osuCurrent.Angle.Value)), 2) / 3.57; + double angle = osuCurrObj.Angle.Value; + + if (angle < pi_over_2) + angleBonus = 1.25; + else if (angle < angle_bonus_begin) + angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - angle)), 2) / 4; - if (osuCurrent.Angle.Value < pi_over_2) - { - angleBonus = 1.28; - if (distance < 90 && osuCurrent.Angle.Value < pi_over_4) - angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1); - else if (distance < 90) - angleBonus += (1 - angleBonus) * Math.Min((90 - distance) / 10, 1) * Math.Sin((pi_over_2 - osuCurrent.Angle.Value) / pi_over_4); - } } - return ((1 + (speedBonus - 1) * 0.75) * angleBonus * speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / osuCurrent.StrainTime; + return (angleBonus * speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / osuCurrObj.StrainTime; } private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); @@ -176,7 +168,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills double deltaTime = Math.Max(max_speed_bonus, current.DeltaTime); if (deltaTime < min_speed_bonus) - speedBonus = 1 + Math.Pow((min_speed_bonus - deltaTime) / speed_balancing_factor, 2); + speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - deltaTime) / speed_balancing_factor, 2); currentRhythm = calculateRhythmBonus(current); From 7b70d41a937b4ebd12dd9c04a56f47e0d1efd6d8 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Thu, 19 Aug 2021 14:49:44 +0000 Subject: [PATCH 006/391] forgot about the / 0 --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 42f65ce4ed..24de290f1f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -173,6 +173,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty { int amountHitObjectsWithAccuracy = Attributes.HitCircleCount; + if (amountHitObjectsWithAccuracy == 0) + return 0; + // This section should be documented by Tr3, but effectively we're calculating the exact same way as before, but // we calculate a variance based on the object count and # of 50s, 100s, etc. This prevents us from having cases // where an SS on lower OD is actually worth more than a 95% on OD11, even though the OD11 requires a greater From b44e6f634d56068564186566894ecc4f53a0c888 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Thu, 19 Aug 2021 15:05:39 +0000 Subject: [PATCH 007/391] noticed a bug with double applying historicaldecay --- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index b29e8be1a2..324546eeb9 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private const double pi_over_4 = Math.PI / 4; private const double pi_over_2 = Math.PI / 2; - private const double rhythmMultiplier = 2.5; + private const double rhythmMultiplier = 2.0; private const int HistoryTimeMax = 3000; // 3 seconds of calculatingRhythmBonus max. private double skillMultiplier => 1375; @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet) effectiveRatio *= 0.25; - rhythmComplexitySum += effectiveRatio * currHistoricalDecay; + rhythmComplexitySum += effectiveRatio; previousIslandSize = islandSize; // log the last island size. From 5b2cfcc2ffeadc92c97b061b861d1754ed25c308 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Thu, 19 Aug 2021 15:27:37 +0000 Subject: [PATCH 008/391] adjusted low acc nerf on speed --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 24de290f1f..bb3c3a4e59 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -161,8 +161,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Scale the speed value with accuracy and OD speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2); - // Punish low OD severely prevent OD abuse on rhythmically complex songs. - speedValue *= Math.Max(0.75, (Math.Min(8, Attributes.OverallDifficulty) / 8)); + // Punish high speed values with low OD to prevent OD abuse on rhythmically complex songs. + if (speedValue > 100 && Attributes.OverallDifficulty < 8) + speedValue = 100 + (speedValue - 100) * Math.Max(0.5, (Attributes.OverallDifficulty - 4) / 4); // Scale the speed value with # of 50s to punish doubletapping. speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); From d36eb269b456ac431ebe8f6d76da85ac7222fae7 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Thu, 19 Aug 2021 20:11:18 +0000 Subject: [PATCH 009/391] fixed code quality issues --- .../Difficulty/OsuPerformanceCalculator.cs | 9 ++++----- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 13 +++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index bb3c3a4e59..80465efe6f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -190,15 +190,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty double m300 = 79.5 - 6.0 * Attributes.OverallDifficulty; double m100 = 139.5 - 8.0 * Attributes.OverallDifficulty; double m50 = 199.5 - 10.0 * Attributes.OverallDifficulty; - double acc = p300 + 1.0 / 3.0 * p100 + 1.0 / 6.0 * p50; double variance = p300 * Math.Pow(m300 / 2.0, 2.0) + - p100 * Math.Pow((m300 + m100) / 2.0, 2.0) + - p50 * Math.Pow((m100 + m50) / 2.0, 2.0) + - pm * Math.Pow(229.5 - 11 * Attributes.OverallDifficulty, 2.0); + p100 * Math.Pow((m300 + m100) / 2.0, 2.0) + + p50 * Math.Pow((m100 + m50) / 2.0, 2.0) + + pm * Math.Pow(229.5 - 11 * Attributes.OverallDifficulty, 2.0); double accuracyValue = 2.83 * Math.Pow(1.52163, (79.5 - 2 * Math.Sqrt(variance)) / 6.0) - * Math.Pow(Math.Log(1.0 + (Math.E - 1.0) * (Math.Min(amountHitObjectsWithAccuracy, 1600) / 1000.0)), 0.5); + * Math.Pow(Math.Log(1.0 + (Math.E - 1.0) * (Math.Min(amountHitObjectsWithAccuracy, 1600) / 1000.0)), 0.5); if (mods.Any(m => m is OsuModHidden)) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 324546eeb9..88861d8bd5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -20,8 +20,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private const double pi_over_4 = Math.PI / 4; private const double pi_over_2 = Math.PI / 2; - private const double rhythmMultiplier = 2.0; - private const int HistoryTimeMax = 3000; // 3 seconds of calculatingRhythmBonus max. + private const double rhythm_multiplier = 2.0; + private const int history_time_max = 3000; // 3 seconds of calculatingRhythmBonus max. private double skillMultiplier => 1375; private double strainDecayBase => 0.3; @@ -65,11 +65,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills for (int i = Previous.Count - 1; i > 0; i--) { - double currHistoricalDecay = Math.Max(0, (HistoryTimeMax - (current.StartTime - Previous[i - 1].StartTime))) / HistoryTimeMax; // scales note 0 to 1 from history to now + double currHistoricalDecay = Math.Max(0, (history_time_max - (current.StartTime - Previous[i - 1].StartTime))) / history_time_max; // scales note 0 to 1 from history to now if (currHistoricalDecay != 0) { - currHistoricalDecay = Math.Max(currHistoricalDecay, (Previous.Count - i) / Previous.Count); // either we're limited by time or limited by object count. + // below was bugged in initial version. fixed now, but will change values, will do more testing + // currHistoricalDecay = Math.Min(currHistoricalDecay, (double)(Previous.Count - i) / Previous.Count); // either we're limited by time or limited by object count. double currDelta = ((OsuDifficultyHitObject)Previous[i - 1]).StrainTime; double prevDelta = ((OsuDifficultyHitObject)Previous[i]).StrainTime; @@ -120,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills } } - return Math.Sqrt(4 + rhythmComplexitySum * rhythmMultiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) + return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) } private double tapStrainOf(DifficultyHitObject current, double speedBonus) @@ -151,7 +152,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (angle < pi_over_2) angleBonus = 1.25; else if (angle < angle_bonus_begin) - angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - angle)), 2) / 4; + angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - angle)), 2) / 4; } From a46ae855aa513b53f5a9e70bb9f98ee96af98cc1 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Fri, 20 Aug 2021 15:40:34 +0000 Subject: [PATCH 010/391] implemented object count limitation for rhythm build up --- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 88861d8bd5..e350a113f7 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -69,8 +69,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (currHistoricalDecay != 0) { - // below was bugged in initial version. fixed now, but will change values, will do more testing - // currHistoricalDecay = Math.Min(currHistoricalDecay, (double)(Previous.Count - i) / Previous.Count); // either we're limited by time or limited by object count. + currHistoricalDecay = Math.Min(currHistoricalDecay, (double)(Previous.Count - i) / Previous.Count); // either we're limited by time or limited by object count. double currDelta = ((OsuDifficultyHitObject)Previous[i - 1]).StrainTime; double prevDelta = ((OsuDifficultyHitObject)Previous[i]).StrainTime; From 812c85f3defa233be7e038e07930e5e22551d142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 21 Aug 2021 16:43:38 +0200 Subject: [PATCH 011/391] Clean up code style issues --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 3 +-- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 80465efe6f..3ed8012a83 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -197,8 +197,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty pm * Math.Pow(229.5 - 11 * Attributes.OverallDifficulty, 2.0); double accuracyValue = 2.83 * Math.Pow(1.52163, (79.5 - 2 * Math.Sqrt(variance)) / 6.0) - * Math.Pow(Math.Log(1.0 + (Math.E - 1.0) * (Math.Min(amountHitObjectsWithAccuracy, 1600) / 1000.0)), 0.5); - + * Math.Pow(Math.Log(1.0 + (Math.E - 1.0) * (Math.Min(amountHitObjectsWithAccuracy, 1600) / 1000.0)), 0.5); if (mods.Any(m => m is OsuModHidden)) accuracyValue *= 1.08; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index e350a113f7..7e3d069abf 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -86,7 +86,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { islandSize++; // island is still progressing, count size. } - else { if (islandSize > 6) @@ -152,7 +151,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills angleBonus = 1.25; else if (angle < angle_bonus_begin) angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - angle)), 2) / 4; - } return (angleBonus * speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / osuCurrObj.StrainTime; From aedfdce87296abec5f9f572408de333bf0a9e8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 21 Aug 2021 16:45:41 +0200 Subject: [PATCH 012/391] Update values in calculator test --- .../OsuDifficultyCalculatorTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 8d8387378e..265a7ccd21 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; - [TestCase(6.7568168283591499d, "diffcalc-test")] - [TestCase(1.0348244046058293d, "zero-length-sliders")] + [TestCase(6.6915334809485199d, "diffcalc-test")] + [TestCase(1.0366129190339499d, "zero-length-sliders")] public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(8.4783236764532557d, "diffcalc-test")] - [TestCase(1.2708532136987165d, "zero-length-sliders")] + [TestCase(8.3966904501824473d, "diffcalc-test")] + [TestCase(1.2732783892964523d, "zero-length-sliders")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new OsuModDoubleTime()); From 270f497af83d472ead4266137a42fbb16af72f20 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Sat, 21 Aug 2021 17:23:17 +0000 Subject: [PATCH 013/391] penalized 1/1->1/2->1/4 rhythm transitions --- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index e350a113f7..58bc5680e2 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private const double pi_over_4 = Math.PI / 4; private const double pi_over_2 = Math.PI / 2; - private const double rhythm_multiplier = 2.0; + private const double rhythm_multiplier = 2.5; private const int history_time_max = 3000; // 3 seconds of calculatingRhythmBonus max. private double skillMultiplier => 1375; @@ -58,12 +58,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills return 0; int previousIslandSize = -1; + int prevPreviousIslandSize = -1; double rhythmComplexitySum = 0; int islandSize = 0; bool firstDeltaSwitch = false; - for (int i = Previous.Count - 1; i > 0; i--) + for (int i = Previous.Count - 2; i > 0; i--) { double currHistoricalDecay = Math.Max(0, (history_time_max - (current.StartTime - Previous[i - 1].StartTime))) / history_time_max; // scales note 0 to 1 from history to now @@ -73,6 +74,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills double currDelta = ((OsuDifficultyHitObject)Previous[i - 1]).StrainTime; double prevDelta = ((OsuDifficultyHitObject)Previous[i]).StrainTime; + double prevPrevDelta = ((OsuDifficultyHitObject)Previous[i + 1]).StrainTime; double effectiveRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta); if (effectiveRatio > 0.5) @@ -98,12 +100,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle effectiveRatio *= 0.5; - if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet) + if (prevPreviousIslandSize % 2 == islandSize % 2) // repeated island size (ex: triplet -> triplet) effectiveRatio *= 0.25; + if (prevPrevDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this. + effectiveRatio *= 0.125; + rhythmComplexitySum += effectiveRatio; previousIslandSize = islandSize; // log the last island size. + prevPreviousIslandSize = previousIslandSize; // yep if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size. From 0402f85eb017e869409fa10a292bcef581afc3ee Mon Sep 17 00:00:00 2001 From: Xexxar Date: Sat, 21 Aug 2021 17:29:17 +0000 Subject: [PATCH 014/391] left residual code from testing prevprevdeltas --- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 059694ad17..21477a09da 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -58,7 +58,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills return 0; int previousIslandSize = -1; - int prevPreviousIslandSize = -1; double rhythmComplexitySum = 0; int islandSize = 0; @@ -99,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle effectiveRatio *= 0.5; - if (prevPreviousIslandSize % 2 == islandSize % 2) // repeated island size (ex: triplet -> triplet) + if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet) effectiveRatio *= 0.25; if (prevPrevDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this. @@ -108,7 +107,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills rhythmComplexitySum += effectiveRatio; previousIslandSize = islandSize; // log the last island size. - prevPreviousIslandSize = previousIslandSize; // yep if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size. From aaffc05b820b434ca74c9ffc8a2af81f8c8d5d0b Mon Sep 17 00:00:00 2001 From: Xexxar Date: Sat, 21 Aug 2021 17:56:45 +0000 Subject: [PATCH 015/391] removed accuracy penalty for misses --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 3ed8012a83..62f84b22ec 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -185,7 +185,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double p100 = (2 * (double)countOk) / amountHitObjectsWithAccuracy; // this is multiplied by two to encourage better accuracy. (scales better) double p50 = (1 * (double)countMeh) / amountHitObjectsWithAccuracy; double pm = (1 * (double)countMiss) / amountHitObjectsWithAccuracy; - double p300 = 1.0 - pm - p100 - p50; + double p300 = Math.Max(0, 1.0 - pm - p100 - p50); double m300 = 79.5 - 6.0 * Attributes.OverallDifficulty; double m100 = 139.5 - 8.0 * Attributes.OverallDifficulty; @@ -193,8 +193,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double variance = p300 * Math.Pow(m300 / 2.0, 2.0) + p100 * Math.Pow((m300 + m100) / 2.0, 2.0) + - p50 * Math.Pow((m100 + m50) / 2.0, 2.0) + - pm * Math.Pow(229.5 - 11 * Attributes.OverallDifficulty, 2.0); + p50 * Math.Pow((m100 + m50) / 2.0, 2.0); double accuracyValue = 2.83 * Math.Pow(1.52163, (79.5 - 2 * Math.Sqrt(variance)) / 6.0) * Math.Pow(Math.Log(1.0 + (Math.E - 1.0) * (Math.Min(amountHitObjectsWithAccuracy, 1600) / 1000.0)), 0.5); From b05963cc503d9474fa77bcb50cb35c0115e6c550 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Sun, 12 Sep 2021 16:08:17 +0000 Subject: [PATCH 016/391] update to fix review issues --- .../Difficulty/Skills/Speed.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 21477a09da..1f708737bf 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -6,6 +6,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; +using osu.Framework.Utils; namespace osu.Game.Rulesets.Osu.Difficulty.Skills { @@ -44,11 +45,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { } - private bool isRatioEqual(double ratio, double a, double b) - { - return a + 15 > ratio * b && a - 15 < ratio * b; - } - /// /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . /// @@ -65,15 +61,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills for (int i = Previous.Count - 2; i > 0; i--) { - double currHistoricalDecay = Math.Max(0, (history_time_max - (current.StartTime - Previous[i - 1].StartTime))) / history_time_max; // scales note 0 to 1 from history to now + DifficultyHitObject currObj = Previous[i - 1]; + DifficultyHitObject prevObj = Previous[i]; + DifficultyHitObject prevPrevObj = Previous[i + 1]; + + double currHistoricalDecay = Math.Max(0, (history_time_max - (current.StartTime - currObj.StartTime))) / history_time_max; // scales note 0 to 1 from history to now if (currHistoricalDecay != 0) { currHistoricalDecay = Math.Min(currHistoricalDecay, (double)(Previous.Count - i) / Previous.Count); // either we're limited by time or limited by object count. - double currDelta = ((OsuDifficultyHitObject)Previous[i - 1]).StrainTime; - double prevDelta = ((OsuDifficultyHitObject)Previous[i]).StrainTime; - double prevPrevDelta = ((OsuDifficultyHitObject)Previous[i + 1]).StrainTime; + double currDelta = ((OsuDifficultyHitObject)currObj).StrainTime; + double prevDelta = ((OsuDifficultyHitObject)prevObj).StrainTime; + double prevPrevDelta = ((OsuDifficultyHitObject)prevPrevObj).StrainTime; double effectiveRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta); if (effectiveRatio > 0.5) @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (firstDeltaSwitch) { - if (isRatioEqual(1.0, prevDelta, currDelta)) + if (Precision.AlmostEquals(prevDelta, currDelta, 15)) { islandSize++; // island is still progressing, count size. } @@ -174,10 +174,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills currentRhythm = calculateRhythmBonus(current); - currentTapStrain *= strainDecay(current.DeltaTime); + double decay = strainDecay(current.DeltaTime); + + currentTapStrain *= decay; currentTapStrain += tapStrainOf(current, speedBonus) * skillMultiplier; - currentMovementStrain *= strainDecay(current.DeltaTime); + currentMovementStrain *= decay; currentMovementStrain += movementStrainOf(current, speedBonus) * skillMultiplier; return currentMovementStrain + currentTapStrain * currentRhythm; From 44163dc9ec1ae5b2cc43c7ebbdde55670bf4f2df Mon Sep 17 00:00:00 2001 From: Xexxar Date: Sun, 12 Sep 2021 18:14:05 +0000 Subject: [PATCH 017/391] updated to use deltaTime not stainTime for more accuracy --- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 1f708737bf..7b99ceac6f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -71,8 +71,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { currHistoricalDecay = Math.Min(currHistoricalDecay, (double)(Previous.Count - i) / Previous.Count); // either we're limited by time or limited by object count. - double currDelta = ((OsuDifficultyHitObject)currObj).StrainTime; - double prevDelta = ((OsuDifficultyHitObject)prevObj).StrainTime; + double currDelta = Math.Max(25, currObj.DeltaTime); + double prevDelta = Math.Max(25, prevObj.DeltaTime); double prevPrevDelta = ((OsuDifficultyHitObject)prevPrevObj).StrainTime; double effectiveRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta); From cdd813926b53558be371f66b193e452babdaae86 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Fri, 17 Sep 2021 00:27:42 +0000 Subject: [PATCH 018/391] nerf to high bpm rhythm changes, nerf to 1/3->1/4 --- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 7b99ceac6f..0e32ea915d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -77,9 +77,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills double effectiveRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta); if (effectiveRatio > 0.5) - effectiveRatio = 0.5 + (effectiveRatio - 0.5) * 10; // large buff for 1/3 -> 1/4 type transitions. + effectiveRatio = 0.5 + (effectiveRatio - 0.5) * 5; // large buff for 1/3 -> 1/4 type transitions. - effectiveRatio *= Math.Sqrt(100 / ((currDelta + prevDelta) / 2)) * currHistoricalDecay; // scale with bpm slightly and with time + effectiveRatio *= currHistoricalDecay; // scale with time if (firstDeltaSwitch) { From 2c071a4d223481f3a14e770eaedcdd14f5415a3c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Sep 2021 14:40:29 +0900 Subject: [PATCH 019/391] Add test coverage of pausing with a large audio offset --- .../Visual/Gameplay/TestScenePause.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index bddc7ab731..324953eb9f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -3,10 +3,12 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Rulesets; @@ -39,6 +41,45 @@ namespace osu.Game.Tests.Visual.Gameplay confirmClockRunning(true); } + [Test] + public void TestPauseWithLargeOffset() + { + double lastTime; + bool alwaysGoingForward = true; + + AddStep("force large offset", () => + { + var offset = (BindableDouble)LocalConfig.GetBindable(OsuSetting.AudioOffset); + + // use a large negative offset to avoid triggering a fail from forwards seeking. + offset.MinValue = -5000; + offset.Value = -5000; + }); + + AddStep("add time forward check hook", () => + { + lastTime = double.MinValue; + alwaysGoingForward = true; + + Player.OnUpdate += _ => + { + double currentTime = Player.GameplayClockContainer.CurrentTime; + alwaysGoingForward &= currentTime >= lastTime; + lastTime = currentTime; + }; + }); + + AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10))); + + pauseAndConfirm(); + + resumeAndConfirm(); + + AddAssert("time didn't go backwards", () => alwaysGoingForward); + + AddStep("reset offset", () => LocalConfig.SetValue(OsuSetting.AudioOffset, 0)); + } + [Test] public void TestPauseResume() { From 5f27f1c099c0b1e369cf01662bcf758de91ae42d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Sep 2021 15:39:03 +0900 Subject: [PATCH 020/391] Avoid accounting for the pause pitch adjust effect when "fixing" hardware offset adjustments Bit of an unfortunate one. Because we are applying the pitch adjustment to the lowest level (`Track`), it's hard to filter out in parent clock calculations. Tried a few solutions but this feels the best. Note that we can't just undo the `pauseFreqAdjust` adjustment as it will div-by-zero. Closes https://github.com/ppy/osu/issues/14773. --- .../Play/MasterGameplayClockContainer.cs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs index 3fbb55872b..aa46522dec 100644 --- a/osu.Game/Screens/Play/MasterGameplayClockContainer.cs +++ b/osu.Game/Screens/Play/MasterGameplayClockContainer.cs @@ -158,10 +158,10 @@ namespace osu.Game.Screens.Play { // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. // This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. - platformOffsetClock = new HardwareCorrectionOffsetClock(source) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; + platformOffsetClock = new HardwareCorrectionOffsetClock(source, pauseFreqAdjust) { Offset = RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? 15 : 0 }; // the final usable gameplay clock with user-set offsets applied. - userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock); + userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock, pauseFreqAdjust); return masterGameplayClock = new MasterGameplayClock(userOffsetClock); } @@ -216,11 +216,25 @@ namespace osu.Game.Screens.Play { // we always want to apply the same real-time offset, so it should be adjusted by the difference in playback rate (from realtime) to achieve this. // base implementation already adds offset at 1.0 rate, so we only add the difference from that here. - public override double CurrentTime => base.CurrentTime + Offset * (Rate - 1); + public override double CurrentTime => base.CurrentTime + offsetAdjust; - public HardwareCorrectionOffsetClock(IClock source, bool processSource = true) - : base(source, processSource) + private readonly BindableDouble pauseRateAdjust; + + private double offsetAdjust; + + public HardwareCorrectionOffsetClock(IClock source, BindableDouble pauseRateAdjust) + : base(source) { + this.pauseRateAdjust = pauseRateAdjust; + } + + public override void ProcessFrame() + { + base.ProcessFrame(); + + // changing this during the pause transform effect will cause a potentially large offset to be suddenly applied as we approach zero rate. + if (pauseRateAdjust.Value == 1) + offsetAdjust = Offset * (Rate - 1); } } From 45caeb84d38eaa4404e6c3dc2bd1ce465ebe3131 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Sep 2021 16:28:47 +0900 Subject: [PATCH 021/391] Fix incorrect type specification when restoring bindable value --- osu.Game.Tests/Visual/Gameplay/TestScenePause.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 324953eb9f..04676f656f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("time didn't go backwards", () => alwaysGoingForward); - AddStep("reset offset", () => LocalConfig.SetValue(OsuSetting.AudioOffset, 0)); + AddStep("reset offset", () => LocalConfig.SetValue(OsuSetting.AudioOffset, 0.0)); } [Test] From 0d58530dbea69e72ef12a8bc1c0fac6fe60f5397 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Sep 2021 01:48:14 +0900 Subject: [PATCH 022/391] Reduce overhead of `ColumnHasObject` calls by storing column usage separately --- .../Beatmaps/Patterns/Pattern.cs | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs index f095a0ffce..817d2b60b6 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; using osu.Game.Rulesets.Mania.Objects; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns @@ -14,6 +13,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns { private readonly List hitObjects = new List(); + private readonly HashSet containedColumns = new HashSet(); + /// /// All the hit objects contained in this pattern. /// @@ -24,34 +25,42 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns /// /// The column index. /// Whether the column with index contains a hit object. - public bool ColumnHasObject(int column) => hitObjects.Exists(h => h.Column == column); + public bool ColumnHasObject(int column) => containedColumns.Contains(column); /// /// Amount of columns taken up by hit objects in this pattern. /// - public int ColumnWithObjects => HitObjects.GroupBy(h => h.Column).Count(); + public int ColumnWithObjects => containedColumns.Count; /// /// Adds a hit object to this pattern. /// /// The hit object to add. - public void Add(ManiaHitObject hitObject) => hitObjects.Add(hitObject); + public void Add(ManiaHitObject hitObject) + { + hitObjects.Add(hitObject); + containedColumns.Add(hitObject.Column); + } /// /// Copies hit object from another pattern to this one. /// /// The other pattern. - public void Add(Pattern other) => hitObjects.AddRange(other.HitObjects); + public void Add(Pattern other) + { + hitObjects.AddRange(other.HitObjects); + + foreach (var h in other.hitObjects) + containedColumns.Add(h.Column); + } /// /// Clears this pattern, removing all hit objects. /// - public void Clear() => hitObjects.Clear(); - - /// - /// Removes a hit object from this pattern. - /// - /// The hit object to remove. - public bool Remove(ManiaHitObject hitObject) => hitObjects.Remove(hitObject); + public void Clear() + { + hitObjects.Clear(); + containedColumns.Clear(); + } } } From 16e60eed56d8586ef69df2eeeb1e5b2fd6b2836f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Sep 2021 01:48:33 +0900 Subject: [PATCH 023/391] Reduce `NestedHitObject` enumerator overhead This was especially bad due to it allocating on any and every start time change, even the first (see usage in `HitObject.ctor`). --- .../Difficulty/CatchDifficultyCalculator.cs | 2 +- osu.Game/Rulesets/Objects/HitObject.cs | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 9feaa55051..82d76252d2 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty // In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream. foreach (var hitObject in beatmap.HitObjects - .SelectMany(obj => obj is JuiceStream stream ? stream.NestedHitObjects : new[] { obj }) + .SelectMany(obj => obj is JuiceStream stream ? stream.NestedHitObjects.AsEnumerable() : new[] { obj }) .Cast() .OrderBy(x => x.StartTime)) { diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index c4b9e6e1ad..ae0cb895bc 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -3,11 +3,12 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading; using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.Bindables; +using osu.Framework.Extensions.ListExtensions; +using osu.Framework.Lists; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -83,7 +84,7 @@ namespace osu.Game.Rulesets.Objects private readonly List nestedHitObjects = new List(); [JsonIgnore] - public IReadOnlyList NestedHitObjects => nestedHitObjects; + public SlimReadOnlyListWrapper NestedHitObjects => nestedHitObjects.AsSlimReadOnly(); public HitObject() { @@ -91,7 +92,7 @@ namespace osu.Game.Rulesets.Objects { double offset = time.NewValue - time.OldValue; - foreach (var nested in NestedHitObjects) + foreach (var nested in nestedHitObjects) nested.StartTime += offset; }; } @@ -122,11 +123,14 @@ namespace osu.Game.Rulesets.Objects if (this is IHasComboInformation hasCombo) { - foreach (var n in NestedHitObjects.OfType()) + foreach (HitObject hitObject in nestedHitObjects) { - n.ComboIndexBindable.BindTo(hasCombo.ComboIndexBindable); - n.ComboIndexWithOffsetsBindable.BindTo(hasCombo.ComboIndexWithOffsetsBindable); - n.IndexInCurrentComboBindable.BindTo(hasCombo.IndexInCurrentComboBindable); + if (hitObject is IHasComboInformation n) + { + n.ComboIndexBindable.BindTo(hasCombo.ComboIndexBindable); + n.ComboIndexWithOffsetsBindable.BindTo(hasCombo.ComboIndexWithOffsetsBindable); + n.IndexInCurrentComboBindable.BindTo(hasCombo.IndexInCurrentComboBindable); + } } } From 03291e389776777c85db8899d5b64328a548b689 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Sep 2021 02:14:20 +0900 Subject: [PATCH 024/391] Avoid LINQ overhead in `PatternGenerator.isValid` --- .../Beatmaps/Patterns/Legacy/PatternGenerator.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs index fb58d805a9..e643b82271 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs @@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { lowerBound ??= RandomStart; upperBound ??= TotalColumns; - nextColumn ??= (_ => GetRandomColumn(lowerBound, upperBound)); + nextColumn ??= _ => GetRandomColumn(lowerBound, upperBound); // Check for the initial column if (isValid(initialColumn)) @@ -176,7 +176,19 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy return initialColumn; - bool isValid(int column) => validation?.Invoke(column) != false && !patterns.Any(p => p.ColumnHasObject(column)); + bool isValid(int column) + { + if (validation?.Invoke(column) == false) + return false; + + foreach (var p in patterns) + { + if (p.ColumnHasObject(column)) + return false; + } + + return true; + } } /// From d96d1b3e47fb57691b5bb72c60e30ea6b847a310 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Sep 2021 14:47:47 +0900 Subject: [PATCH 025/391] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 4859510e6c..7a739e6f2a 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 0460de6d72..2bed1b1564 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 51ca381b63..b547e98419 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From 98f1c1cc292d5103523b8e1a87b284ae01f5bbfc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Sep 2021 16:02:02 +0900 Subject: [PATCH 026/391] Avoid allocating list storage in `Pattern` until first usage Patterns can often be constructed only to never be used. --- .../Beatmaps/Patterns/Pattern.cs | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs index 817d2b60b6..8c42e47dbc 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Mania.Objects; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns @@ -11,26 +12,25 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns /// internal class Pattern { - private readonly List hitObjects = new List(); - - private readonly HashSet containedColumns = new HashSet(); + private List hitObjects; + private HashSet containedColumns; /// /// All the hit objects contained in this pattern. /// - public IEnumerable HitObjects => hitObjects; + public IEnumerable HitObjects => hitObjects ?? Enumerable.Empty(); /// /// Check whether a column of this patterns contains a hit object. /// /// The column index. /// Whether the column with index contains a hit object. - public bool ColumnHasObject(int column) => containedColumns.Contains(column); + public bool ColumnHasObject(int column) => containedColumns?.Contains(column) ?? false; /// /// Amount of columns taken up by hit objects in this pattern. /// - public int ColumnWithObjects => containedColumns.Count; + public int ColumnWithObjects => containedColumns?.Count ?? 0; /// /// Adds a hit object to this pattern. @@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns /// The hit object to add. public void Add(ManiaHitObject hitObject) { + prepareStorage(); + hitObjects.Add(hitObject); containedColumns.Add(hitObject.Column); } @@ -48,10 +50,15 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns /// The other pattern. public void Add(Pattern other) { - hitObjects.AddRange(other.HitObjects); + prepareStorage(); - foreach (var h in other.hitObjects) - containedColumns.Add(h.Column); + if (other.hitObjects != null) + { + hitObjects.AddRange(other.hitObjects); + + foreach (var h in other.hitObjects) + containedColumns.Add(h.Column); + } } /// @@ -59,8 +66,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns /// public void Clear() { - hitObjects.Clear(); - containedColumns.Clear(); + hitObjects?.Clear(); + containedColumns?.Clear(); + } + + private void prepareStorage() + { + hitObjects ??= new List(); + containedColumns ??= new HashSet(); } } } From 93e33fa94d666130e9628980acfcd98c6c5536e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Sep 2021 14:08:54 +0900 Subject: [PATCH 027/391] Use `true` comparison rather than null coalesce fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs index 8c42e47dbc..828f2ec393 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Pattern.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns /// /// The column index. /// Whether the column with index contains a hit object. - public bool ColumnHasObject(int column) => containedColumns?.Contains(column) ?? false; + public bool ColumnHasObject(int column) => containedColumns?.Contains(column) == true; /// /// Amount of columns taken up by hit objects in this pattern. From aa0d32b3b109bdc62bc8f7596baef3bce774cfa8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Sep 2021 14:23:44 +0900 Subject: [PATCH 028/391] Retarget `master` repos in diffcalc CI runs --- .github/workflows/diffcalc.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index 842522ae87..3c57d971ae 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -78,8 +78,7 @@ jobs: - name: Checkout osu (master) uses: actions/checkout@v2 with: - repository: peppy/osu - ref: 'diffcalc-optimisations' + repository: ppy/osu path: 'master/osu' - name: Checkout osu (pr) uses: actions/checkout@v2 @@ -89,14 +88,12 @@ jobs: - name: Checkout osu-difficulty-calculator (master) uses: actions/checkout@v2 with: - repository: peppy/osu-difficulty-calculator - ref: 'bypass-attrib-row-insert' + repository: ppy/osu-difficulty-calculator path: 'master/osu-difficulty-calculator' - name: Checkout osu-difficulty-calculator (pr) uses: actions/checkout@v2 with: - repository: peppy/osu-difficulty-calculator - ref: 'bypass-attrib-row-insert' + repository: ppy/osu-difficulty-calculator path: 'pr/osu-difficulty-calculator' - name: Install .NET 5.0.x From 78e7755df1abe54f47d7b88215fa7d1a2418d20a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Sep 2021 17:54:09 +0900 Subject: [PATCH 029/391] Re-enable IPC import test --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index b2bd60d342..cba7f34ede 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -582,7 +582,6 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] [NonParallelizable] - [Ignore("Binding IPC on Appveyor isn't working (port in use). Need to figure out why")] public void TestImportOverIPC() { using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-host", true)) From 3b174a71a3e73976ca591719ff37a5f3e1aac93c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Sep 2021 18:00:54 +0900 Subject: [PATCH 030/391] Move startup concurrency check to higher level Makes more sense to perform this check outside of the game itself now that we can. --- osu.Desktop/Program.cs | 5 ++++- osu.Game/OsuGame.cs | 6 ------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index dc712f2593..7434dec010 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -74,7 +74,10 @@ namespace osu.Desktop // we want to allow multiple instances to be started when in debug. if (!DebugUtils.IsDebugBuild) - return 0; + { + Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error); + Environment.Exit(0); + } } if (tournamentClient) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 0e55a65aec..dde60158d1 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -210,12 +210,6 @@ namespace osu.Game [BackgroundDependencyLoader] private void load() { - if (!Host.IsPrimaryInstance && !DebugUtils.IsDebugBuild) - { - Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error); - Environment.Exit(0); - } - if (args?.Length > 0) { var paths = args.Where(a => !a.StartsWith('-')).ToArray(); From 5e53058fbc39415b3b72c61e028b3e1805094f1d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Sep 2021 18:03:43 +0900 Subject: [PATCH 031/391] Use same method of exiting execution as previously for safety --- 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 7434dec010..8ccd23b418 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -76,7 +76,7 @@ namespace osu.Desktop if (!DebugUtils.IsDebugBuild) { Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error); - Environment.Exit(0); + return 0; } } From 2bac15ca1a5631a725179986a7e5c73a62efdeca Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Wed, 22 Sep 2021 14:17:27 +0100 Subject: [PATCH 032/391] base formula implementation --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index bf4d92652c..b5f5821823 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -49,6 +49,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (mods.Any(m => m is OsuModSpunOut)) multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85); + if (mods.Any(m => m is OsuModBlinds)) + multiplier *= 1.12 + (totalHits * (0.0008 / (1 + 3 * countMiss))); + + double aimValue = computeAimValue(); double speedValue = computeSpeedValue(); double accuracyValue = computeAccuracyValue(); From 85fd4bdbf888660f47f2430c3d6e2da8b20fa33a Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Wed, 22 Sep 2021 16:19:41 +0100 Subject: [PATCH 033/391] add accuracy nerf --- 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 b5f5821823..51e742e71c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85); if (mods.Any(m => m is OsuModBlinds)) - multiplier *= 1.12 + (totalHits * (0.0008 / (1 + 3 * countMiss))); + multiplier *= 1.0 + ((0.12 + totalHits * (0.0008 / (1 + 3 * countMiss))) * Math.Pow(accuracy, 16)); double aimValue = computeAimValue(); From e52621c60f2e78b884385364a09cc33c3d7ef627 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Wed, 22 Sep 2021 16:38:50 +0100 Subject: [PATCH 034/391] basically disable HD pp when blinds enabled --- .../Difficulty/OsuPerformanceCalculator.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 51e742e71c..f38208aa94 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -48,11 +48,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (mods.Any(m => m is OsuModSpunOut)) multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85); - - if (mods.Any(m => m is OsuModBlinds)) - multiplier *= 1.0 + ((0.12 + totalHits * (0.0008 / (1 + 3 * countMiss))) * Math.Pow(accuracy, 16)); - - + double aimValue = computeAimValue(); double speedValue = computeSpeedValue(); double accuracyValue = computeAccuracyValue(); @@ -113,7 +109,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor; // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. - if (mods.Any(h => h is OsuModHidden)) + if (mods.Any(m => m is OsuModBlinds)) + aimValue *= 1.0 + ((0.12 + totalHits * (0.0008 / (1 + 3 * countMiss))) * Math.Pow(accuracy, 16)); + else if (mods.Any(h => h is OsuModHidden)) aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); aimValue *= approachRateBonus; @@ -151,7 +149,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor; - if (mods.Any(m => m is OsuModHidden)) + if (mods.Any(m => m is OsuModBlinds)) + speedValue *= 1.0 + ((0.12 + totalHits * (0.0008 / (1 + 3 * countMiss))) * Math.Pow(accuracy, 16)); + else if (mods.Any(m => m is OsuModHidden)) speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); // Scale the speed value with accuracy and OD. From 857ce721da7803cc12dad4e2d92d239c0572ec4f Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Wed, 22 Sep 2021 16:48:11 +0100 Subject: [PATCH 035/391] share BlindsMultiplier everywhere, make it completely negate HD pp --- .../Difficulty/OsuPerformanceCalculator.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index f38208aa94..a15a6d5ea1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -110,7 +110,7 @@ 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. if (mods.Any(m => m is OsuModBlinds)) - aimValue *= 1.0 + ((0.12 + totalHits * (0.0008 / (1 + 3 * countMiss))) * Math.Pow(accuracy, 16)); + aimValue *= blindsMultiplier; else if (mods.Any(h => h is OsuModHidden)) aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); @@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor; if (mods.Any(m => m is OsuModBlinds)) - speedValue *= 1.0 + ((0.12 + totalHits * (0.0008 / (1 + 3 * countMiss))) * Math.Pow(accuracy, 16)); + speedValue *= blindsMultiplier; else if (mods.Any(m => m is OsuModHidden)) speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); @@ -184,7 +184,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Bonus for many hitcircles - it's harder to keep good accuracy up for longer. accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3)); - if (mods.Any(m => m is OsuModHidden)) + if (mods.Any(m => m is OsuModBlinds)) + accuracyValue *= blindsMultiplier; + else if (mods.Any(m => m is OsuModHidden)) accuracyValue *= 1.08; if (mods.Any(m => m is OsuModFlashlight)) accuracyValue *= 1.02; @@ -230,5 +232,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; + private double blindsMultiplier => 1.0 + ((0.12 + totalHits * (0.0008 / (1 + 3 * countMiss))) * Math.Pow(accuracy, 16)); } } From c62e429cea7342652450561966a99611f0fb865b Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Wed, 22 Sep 2021 19:02:25 +0100 Subject: [PATCH 036/391] buff miss factor --- 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 a15a6d5ea1..8f2f286d8b 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -232,6 +232,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; - private double blindsMultiplier => 1.0 + ((0.12 + totalHits * (0.0008 / (1 + 3 * countMiss))) * Math.Pow(accuracy, 16)); + private double blindsMultiplier => 1.0 + ((0.12 + totalHits * (0.0008 / (1 + 2 * countMiss))) * Math.Pow(accuracy, 16)); } } From 70119d3a43d29de697fe2eba5b8d1515d66e5fe4 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Fri, 24 Sep 2021 15:02:19 +0100 Subject: [PATCH 037/391] add drain rate term --- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs | 1 + osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs | 2 ++ osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index ac77a93239..bd4c0f2ad5 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -12,6 +12,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty public double FlashlightRating { get; set; } public double ApproachRate { get; set; } public double OverallDifficulty { get; set; } + public double DrainRate { get; set; } public int HitCircleCount { get; set; } public int SpinnerCount { get; set; } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 4c8d0b2ce6..ab5adbcc3e 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -54,6 +54,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; + double drainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate; int maxCombo = beatmap.HitObjects.Count; // Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above) @@ -71,6 +72,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty FlashlightRating = flashlightRating, ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, OverallDifficulty = (80 - hitWindowGreat) / 6, + DrainRate = drainRate, MaxCombo = maxCombo, HitCircleCount = hitCirclesCount, SpinnerCount = spinnerCount, diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 8f2f286d8b..4d0a4bcde8 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -232,6 +232,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; - private double blindsMultiplier => 1.0 + ((0.12 + totalHits * (0.0008 / (1 + 2 * countMiss))) * Math.Pow(accuracy, 16)); + private double blindsMultiplier => 1.0 + ((0.12 + totalHits * (0.0008 / (1 + 2 * countMiss))) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate); } } From 2508171d41e5a27f6fff9c6116d9c585a2e75391 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Sat, 25 Sep 2021 03:52:05 +0000 Subject: [PATCH 038/391] nerfed rhythm further to attempt to balance --- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index a5af3eb534..01d50c1f0d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private const double pi_over_4 = Math.PI / 4; private const double pi_over_2 = Math.PI / 2; - private const double rhythm_multiplier = 0.75; + private const double rhythm_multiplier = 0.5; private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max. private double skillMultiplier => 1375; From 9b9c30c8a13bfa6b724f691d5c2b11dd76078a68 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Fri, 24 Sep 2021 21:57:52 -0700 Subject: [PATCH 039/391] Fix text overflowing on dropdown headers --- .../Graphics/UserInterface/OsuDropdown.cs | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 42f628a75a..fe88e6f78a 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -274,21 +274,40 @@ namespace osu.Game.Graphics.UserInterface CornerRadius = corner_radius; Height = 40; - Foreground.Children = new Drawable[] + Foreground.Child = new GridContainer { - Text = new OsuSpriteText + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + new Dimension(GridSizeMode.AutoSize), }, - Icon = new SpriteIcon + ColumnDimensions = new[] { - Icon = FontAwesome.Solid.ChevronDown, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Margin = new MarginPadding { Right = 5 }, - Size = new Vector2(12), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), }, + Content = new[] + { + new Drawable[] + { + Text = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Truncate = true, + }, + Icon = new SpriteIcon + { + Icon = FontAwesome.Solid.ChevronDown, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Margin = new MarginPadding { Horizontal = 5 }, + Size = new Vector2(12), + }, + } + } }; AddInternal(new HoverClickSounds()); From 4099726d594b00f02456131bb7ca3cf76632fd79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 25 Sep 2021 15:41:06 +0200 Subject: [PATCH 040/391] Update SR values in tests --- .../OsuDifficultyCalculatorTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 19881b5c33..c85ce49ef6 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; - [TestCase(6.6634445062299665d, "diffcalc-test")] - [TestCase(1.0414203870195022d, "zero-length-sliders")] + [TestCase(6.4766744062777439d, "diffcalc-test")] + [TestCase(0.63190213974653298d, "zero-length-sliders")] public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(8.3858089051603368d, "diffcalc-test")] - [TestCase(1.2723279173428435d, "zero-length-sliders")] + [TestCase(8.9926029916156764d, "diffcalc-test")] + [TestCase(0.78116445583636385d, "zero-length-sliders")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new OsuModDoubleTime()); From b6f494cbb76cde8234a6e0ede820f3f7434b063a Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Sat, 25 Sep 2021 17:34:24 +0100 Subject: [PATCH 041/391] accuracy and speed changes pt 1 --- .../Difficulty/OsuPerformanceCalculator.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 4d0a4bcde8..621fb4588e 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -110,7 +110,7 @@ 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. if (mods.Any(m => m is OsuModBlinds)) - aimValue *= blindsMultiplier; + aimValue *= 1.0 + blindsMultiplier; else if (mods.Any(h => h is OsuModHidden)) aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); @@ -149,8 +149,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor; + // Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given. if (mods.Any(m => m is OsuModBlinds)) - speedValue *= blindsMultiplier; + speedValue *= 1.12; else if (mods.Any(m => m is OsuModHidden)) speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); @@ -184,8 +185,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Bonus for many hitcircles - it's harder to keep good accuracy up for longer. accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3)); + // Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. if (mods.Any(m => m is OsuModBlinds)) - accuracyValue *= blindsMultiplier; + accuracyValue *= 1.14; else if (mods.Any(m => m is OsuModHidden)) accuracyValue *= 1.08; if (mods.Any(m => m is OsuModFlashlight)) @@ -232,6 +234,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; - private double blindsMultiplier => 1.0 + ((0.12 + totalHits * (0.0008 / (1 + 2 * countMiss))) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate); + private double blindsMultiplier => (0.12 + totalHits * (0.0008 / (1 + 2 * countMiss)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate); } } From 4aadff3fd795badade0f0c7816046d1180ff7de0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 26 Sep 2021 15:08:43 +0200 Subject: [PATCH 042/391] Add failing test for incorrect composer selection --- .../Editing/TestSceneBlueprintSelection.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs index 976bf93c15..0dff6d9dd9 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs @@ -7,9 +7,13 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Tests.Beatmaps; using osuTK; @@ -66,5 +70,36 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("selection is unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == firstSlider); } + + [Test] + public void TestOverlappingObjectsWithSameStartTime() + { + AddStep("add overlapping circles", () => + { + EditorBeatmap.Add(createHitCircle(50, OsuPlayfield.BASE_SIZE / 2)); + EditorBeatmap.Add(createHitCircle(50, OsuPlayfield.BASE_SIZE / 2 + new Vector2(-10, -20))); + EditorBeatmap.Add(createHitCircle(50, OsuPlayfield.BASE_SIZE / 2 + new Vector2(10, -20))); + }); + + AddStep("click at centre of playfield", () => + { + var hitObjectContainer = Editor.ChildrenOfType().Single(); + var centre = hitObjectContainer.ToScreenSpace(OsuPlayfield.BASE_SIZE / 2); + InputManager.MoveMouseTo(centre); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("frontmost object selected", () => + { + var hasCombo = Editor.ChildrenOfType().Single(b => b.IsSelected).Item as IHasComboInformation; + return hasCombo?.IndexInCurrentCombo == 0; + }); + } + + private HitCircle createHitCircle(double startTime, Vector2 position) => new HitCircle + { + StartTime = startTime, + Position = position, + }; } } From 0057400bb761750d6d52815388e0b809a90d7e54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 26 Sep 2021 15:09:30 +0200 Subject: [PATCH 043/391] Rename test scene to reflect its new purpose --- ...SceneBlueprintSelection.cs => TestSceneBlueprintOrdering.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Tests/Visual/Editing/{TestSceneBlueprintSelection.cs => TestSceneBlueprintOrdering.cs} (98%) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs similarity index 98% rename from osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs rename to osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs index 0dff6d9dd9..955069f509 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs @@ -21,7 +21,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Editing { - public class TestSceneBlueprintSelection : EditorTestScene + public class TestSceneBlueprintOrdering : EditorTestScene { protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); From ce70d1082d68d938250509856c1bd6b0f9a4f1ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 26 Sep 2021 15:29:00 +0200 Subject: [PATCH 044/391] Add failing test for "ghost timeline blueprint" --- .../Editing/TestSceneBlueprintOrdering.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs index 955069f509..5d8a6dabd7 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintOrdering.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Compose.Components; +using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Tests.Beatmaps; using osuTK; using osuTK.Input; @@ -96,6 +97,32 @@ namespace osu.Game.Tests.Visual.Editing }); } + [Test] + public void TestPlacementOfConcurrentObjectWithDuration() + { + AddStep("seek to timing point", () => EditorClock.Seek(2170)); + AddStep("add hit circle", () => EditorBeatmap.Add(createHitCircle(2170, Vector2.Zero))); + + AddStep("choose spinner placement tool", () => + { + InputManager.Key(Key.Number4); + var hitObjectContainer = Editor.ChildrenOfType().Single(); + InputManager.MoveMouseTo(hitObjectContainer.ScreenSpaceDrawQuad.Centre); + }); + + AddStep("begin placing spinner", () => + { + InputManager.Click(MouseButton.Left); + }); + AddStep("end placing spinner", () => + { + EditorClock.Seek(2500); + InputManager.Click(MouseButton.Right); + }); + + AddAssert("two timeline blueprints present", () => Editor.ChildrenOfType().Count() == 2); + } + private HitCircle createHitCircle(double startTime, Vector2 position) => new HitCircle { StartTime = startTime, From a86b9893acd65219aef8dd0c737125f35f32a5a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 26 Sep 2021 15:48:56 +0200 Subject: [PATCH 045/391] Always re-sort blueprints before adding/removing one --- .../HitObjectOrderedSelectionContainer.cs | 66 +++++++------------ 1 file changed, 24 insertions(+), 42 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs index 4078661a26..eebaa25af1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs @@ -1,8 +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.Bindables; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Edit; @@ -15,53 +14,28 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public sealed class HitObjectOrderedSelectionContainer : Container> { + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + editorBeatmap.HitObjectUpdated += hitObjectUpdated; + } + + private void hitObjectUpdated(HitObject _) => SortInternal(); + public override void Add(SelectionBlueprint drawable) { + SortInternal(); base.Add(drawable); - bindStartTime(drawable); } public override bool Remove(SelectionBlueprint drawable) { - if (!base.Remove(drawable)) - return false; - - unbindStartTime(drawable); - return true; - } - - public override void Clear(bool disposeChildren) - { - base.Clear(disposeChildren); - unbindAllStartTimes(); - } - - private readonly Dictionary, IBindable> startTimeMap = new Dictionary, IBindable>(); - - private void bindStartTime(SelectionBlueprint blueprint) - { - var bindable = blueprint.Item.StartTimeBindable.GetBoundCopy(); - - bindable.BindValueChanged(_ => - { - if (LoadState >= LoadState.Ready) - SortInternal(); - }); - - startTimeMap[blueprint] = bindable; - } - - private void unbindStartTime(SelectionBlueprint blueprint) - { - startTimeMap[blueprint].UnbindAll(); - startTimeMap.Remove(blueprint); - } - - private void unbindAllStartTimes() - { - foreach (var kvp in startTimeMap) - kvp.Value.UnbindAll(); - startTimeMap.Clear(); + SortInternal(); + return base.Remove(drawable); } protected override int Compare(Drawable x, Drawable y) @@ -79,5 +53,13 @@ namespace osu.Game.Screens.Edit.Compose.Components return i == 0 ? CompareReverseChildID(y, x) : i; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (editorBeatmap != null) + editorBeatmap.HitObjectUpdated -= hitObjectUpdated; + } } } From 6dc3e66c930518c72889319a5c618a23148c51a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 26 Sep 2021 15:52:25 +0200 Subject: [PATCH 046/391] Include combo information when determining ordering if available --- .../HitObjectOrderedSelectionContainer.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs index eebaa25af1..e8286c5128 100644 --- a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Screens.Edit.Compose.Components { @@ -45,13 +46,23 @@ namespace osu.Game.Screens.Edit.Compose.Components // Put earlier blueprints towards the end of the list, so they handle input first int i = yObj.Item.StartTime.CompareTo(xObj.Item.StartTime); - if (i != 0) return i; // Fall back to end time if the start time is equal. i = yObj.Item.GetEndTime().CompareTo(xObj.Item.GetEndTime()); + if (i != 0) return i; - return i == 0 ? CompareReverseChildID(y, x) : i; + // As a final fallback, use combo information if available. + if (xObj.Item is IHasComboInformation xHasCombo && yObj.Item is IHasComboInformation yHasCombo) + { + i = yHasCombo.ComboIndex.CompareTo(xHasCombo.ComboIndex); + if (i != 0) return i; + + i = yHasCombo.IndexInCurrentCombo.CompareTo(xHasCombo.IndexInCurrentCombo); + if (i != 0) return i; + } + + return CompareReverseChildID(y, x); } protected override void Dispose(bool isDisposing) From 7caa0a81ec5525f5bbcd3599737e7b14aa2057da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 26 Sep 2021 17:22:58 +0200 Subject: [PATCH 047/391] Rename `TestScene{Editor -> Composer}Selection` In line with an upcoming split in functionality between the composer blueprint container and the timeline blueprint container. --- ...ceneEditorSelection.cs => TestSceneComposerSelection.cs} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename osu.Game.Tests/Visual/Editing/{TestSceneEditorSelection.cs => TestSceneComposerSelection.cs} (98%) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs similarity index 98% rename from osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs rename to osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs index 4c4a87972f..a2a7b72283 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposerSelection.cs @@ -20,14 +20,14 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Editing { - public class TestSceneEditorSelection : EditorTestScene + public class TestSceneComposerSelection : EditorTestScene { protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); - private EditorBlueprintContainer blueprintContainer - => Editor.ChildrenOfType().First(); + private ComposeBlueprintContainer blueprintContainer + => Editor.ChildrenOfType().First(); private void moveMouseToObject(Func targetFunc) { From 0de7db5840573d818f89a5607f43e0c68404f234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 26 Sep 2021 17:41:20 +0200 Subject: [PATCH 048/391] Add test coverage for timeline selection logic --- .../Editing/TestSceneTimelineSelection.cs | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs new file mode 100644 index 0000000000..0a1ab522a1 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs @@ -0,0 +1,163 @@ +// 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; +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.Screens.Edit.Compose.Components.Timeline; +using osu.Game.Tests.Beatmaps; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneTimelineSelection : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + + private TimelineBlueprintContainer blueprintContainer + => Editor.ChildrenOfType().First(); + + private void moveMouseToObject(Func targetFunc) + { + AddStep("move mouse to object", () => + { + var pos = blueprintContainer.SelectionBlueprints + .First(s => s.Item == targetFunc()) + .ChildrenOfType() + .First().ScreenSpaceDrawQuad.Centre; + + InputManager.MoveMouseTo(pos); + }); + } + + [Test] + public void TestNudgeSelection() + { + HitCircle[] addedObjects = null; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects = new[] + { + new HitCircle { StartTime = 100 }, + new HitCircle { StartTime = 200, Position = new Vector2(100) }, + new HitCircle { StartTime = 300, Position = new Vector2(200) }, + new HitCircle { StartTime = 400, Position = new Vector2(300) }, + })); + + AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.AddRange(addedObjects)); + + AddStep("nudge forwards", () => InputManager.Key(Key.K)); + AddAssert("objects moved forwards in time", () => addedObjects[0].StartTime > 100); + + AddStep("nudge backwards", () => InputManager.Key(Key.J)); + AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 100); + } + + [Test] + public void TestBasicSelect() + { + var addedObject = new HitCircle { StartTime = 100 }; + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + moveMouseToObject(() => addedObject); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + + AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject); + + var addedObject2 = new HitCircle + { + StartTime = 200, + Position = new Vector2(100), + }; + + AddStep("add one more hitobject", () => EditorBeatmap.Add(addedObject2)); + AddAssert("selection unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject); + + moveMouseToObject(() => addedObject2); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject2); + } + + [Test] + public void TestMultiSelect() + { + var addedObjects = new[] + { + new HitCircle { StartTime = 100 }, + new HitCircle { StartTime = 200, Position = new Vector2(100) }, + new HitCircle { StartTime = 300, Position = new Vector2(200) }, + new HitCircle { StartTime = 400, Position = new Vector2(300) }, + }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); + + moveMouseToObject(() => addedObjects[0]); + AddStep("click first", () => InputManager.Click(MouseButton.Left)); + + AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObjects[0]); + + AddStep("hold control", () => InputManager.PressKey(Key.ControlLeft)); + + moveMouseToObject(() => addedObjects[1]); + AddStep("click second", () => InputManager.Click(MouseButton.Left)); + AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1])); + + moveMouseToObject(() => addedObjects[2]); + AddStep("click third", () => InputManager.Click(MouseButton.Left)); + AddAssert("3 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 3 && EditorBeatmap.SelectedHitObjects.Contains(addedObjects[2])); + + moveMouseToObject(() => addedObjects[1]); + AddStep("click second", () => InputManager.Click(MouseButton.Left)); + AddAssert("2 hitobjects selected", () => EditorBeatmap.SelectedHitObjects.Count == 2 && !EditorBeatmap.SelectedHitObjects.Contains(addedObjects[1])); + + AddStep("release control", () => InputManager.ReleaseKey(Key.ControlLeft)); + } + + [Test] + public void TestBasicDeselect() + { + var addedObject = new HitCircle { StartTime = 100 }; + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + moveMouseToObject(() => addedObject); + AddStep("left click", () => InputManager.Click(MouseButton.Left)); + + AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObject); + + AddStep("click away", () => + { + InputManager.MoveMouseTo(Editor.ChildrenOfType().Single().ScreenSpaceDrawQuad.TopLeft + Vector2.One); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("selection lost", () => EditorBeatmap.SelectedHitObjects.Count == 0); + } + + [Test] + public void TestQuickDelete() + { + var addedObject = new HitCircle + { + StartTime = 0, + }; + + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + moveMouseToObject(() => addedObject); + + AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); + AddStep("right click", () => InputManager.Click(MouseButton.Right)); + AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); + + AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0); + } + } +} From 81d160c85a0e75b5994abcbda27a9ea9fde28bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 26 Sep 2021 17:45:47 +0200 Subject: [PATCH 049/391] Add test covering expected UX of range selection --- .../Editing/TestSceneTimelineSelection.cs | 68 +++++++++++++++++++ .../Timeline/TimelineSelectionHandler.cs | 33 +++++++++ 2 files changed, 101 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs index 0a1ab522a1..f4703b79f0 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.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 NUnit.Framework; using osu.Framework.Testing; @@ -159,5 +160,72 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0); } + + [Test] + public void TestRangeSelect() + { + var addedObjects = new[] + { + new HitCircle { StartTime = 100 }, + new HitCircle { StartTime = 200, Position = new Vector2(100) }, + new HitCircle { StartTime = 300, Position = new Vector2(200) }, + new HitCircle { StartTime = 400, Position = new Vector2(300) }, + new HitCircle { StartTime = 500, Position = new Vector2(400) }, + }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); + + moveMouseToObject(() => addedObjects[1]); + AddStep("click second", () => InputManager.Click(MouseButton.Left)); + + AddAssert("hitobject selected", () => EditorBeatmap.SelectedHitObjects.Single() == addedObjects[1]); + + AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); + + moveMouseToObject(() => addedObjects[3]); + AddStep("click fourth", () => InputManager.Click(MouseButton.Left)); + assertSelectionIs(addedObjects.Skip(1).Take(3)); + + moveMouseToObject(() => addedObjects[0]); + AddStep("click first", () => InputManager.Click(MouseButton.Left)); + assertSelectionIs(addedObjects.Take(2)); + + AddStep("clear selection", () => EditorBeatmap.SelectedHitObjects.Clear()); + AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); + + moveMouseToObject(() => addedObjects[0]); + AddStep("click first", () => InputManager.Click(MouseButton.Left)); + assertSelectionIs(addedObjects.Take(1)); + + AddStep("hold ctrl", () => InputManager.PressKey(Key.ControlLeft)); + moveMouseToObject(() => addedObjects[2]); + AddStep("click third", () => InputManager.Click(MouseButton.Left)); + assertSelectionIs(new[] { addedObjects[0], addedObjects[2] }); + + AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); + moveMouseToObject(() => addedObjects[4]); + AddStep("click fifth", () => InputManager.Click(MouseButton.Left)); + assertSelectionIs(addedObjects.Except(new[] { addedObjects[1] })); + + moveMouseToObject(() => addedObjects[0]); + AddStep("click first", () => InputManager.Click(MouseButton.Left)); + assertSelectionIs(addedObjects); + + AddStep("clear selection", () => EditorBeatmap.SelectedHitObjects.Clear()); + moveMouseToObject(() => addedObjects[0]); + AddStep("click first", () => InputManager.Click(MouseButton.Left)); + assertSelectionIs(addedObjects.Take(1)); + + moveMouseToObject(() => addedObjects[1]); + AddStep("click first", () => InputManager.Click(MouseButton.Left)); + assertSelectionIs(addedObjects.Take(2)); + + moveMouseToObject(() => addedObjects[2]); + AddStep("click first", () => InputManager.Click(MouseButton.Left)); + assertSelectionIs(addedObjects.Take(3)); + } + + private void assertSelectionIs(IEnumerable hitObjects) + => AddAssert("correct hitobjects selected", () => EditorBeatmap.SelectedHitObjects.OrderBy(h => h.StartTime).SequenceEqual(hitObjects)); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineSelectionHandler.cs index c43e554b85..ff385b0ab3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineSelectionHandler.cs @@ -1,13 +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.Linq; using osu.Framework.Allocation; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Input.Bindings; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osuTK; +using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { @@ -62,5 +65,35 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline EditorBeatmap.Update(h); }); } + + internal override bool MouseDownSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) + { + if (e.ShiftPressed && e.Button == MouseButton.Left && SelectedItems.Any()) + { + handleRangeSelection(blueprint); + return true; + } + + return base.MouseDownSelectionRequested(blueprint, e); + } + + private void handleRangeSelection(SelectionBlueprint blueprint) + { + var clickedObject = blueprint.Item; + double rangeStart = clickedObject.StartTime; + double rangeEnd = clickedObject.GetEndTime(); + + foreach (var selectedObject in SelectedItems) + { + rangeStart = Math.Min(rangeStart, selectedObject.StartTime); + rangeEnd = Math.Max(rangeEnd, selectedObject.GetEndTime()); + } + + EditorBeatmap.SelectedHitObjects.Clear(); + EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects.Where(obj => isInRange(obj, rangeStart, rangeEnd))); + + bool isInRange(HitObject hitObject, double start, double end) + => hitObject.StartTime >= start && hitObject.GetEndTime() <= end; + } } } From d3203f83dd538efdee901ac8eefca79694e3291b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 26 Sep 2021 19:07:38 +0200 Subject: [PATCH 050/391] Add implementation of range selection --- .../Compose/Components/SelectionHandler.cs | 2 +- .../Timeline/TimelineSelectionHandler.cs | 48 +++++++++++++++---- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 44eb062db8..ee35b6a47c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -191,7 +191,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The blueprint. /// The mouse event responsible for selection. /// Whether a selection was performed. - internal bool MouseDownSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) + internal virtual bool MouseDownSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) { if (e.ShiftPressed && e.Button == MouseButton.Right) { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineSelectionHandler.cs index ff385b0ab3..ad770b40b5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineSelectionHandler.cs @@ -2,7 +2,10 @@ // 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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; @@ -66,31 +69,58 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }); } + /// + /// The "pivot" object, used in range selection mode. + /// When in range selection, the range to select is determined by the pivot object + /// (last existing object interacted with prior to holding down Shift) + /// and by the object clicked last when Shift was pressed. + /// + [CanBeNull] + private HitObject pivot; + internal override bool MouseDownSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) { if (e.ShiftPressed && e.Button == MouseButton.Left && SelectedItems.Any()) { - handleRangeSelection(blueprint); + handleRangeSelection(blueprint, e.ControlPressed); return true; } - return base.MouseDownSelectionRequested(blueprint, e); + bool result = base.MouseDownSelectionRequested(blueprint, e); + // ensure that the object wasn't removed by the base implementation before making it the new pivot. + if (EditorBeatmap.HitObjects.Contains(blueprint.Item)) + pivot = blueprint.Item; + return result; } - private void handleRangeSelection(SelectionBlueprint blueprint) + /// + /// Handles a request for range selection (triggered when Shift is held down). + /// + /// The blueprint which was clicked in range selection mode. + /// + /// Whether the selection should be cumulative. + /// In cumulative mode, consecutive range selections will shift the pivot (which usually stays fixed for the duration of a range selection) + /// and will never deselect an object that was previously selected. + /// + private void handleRangeSelection(SelectionBlueprint blueprint, bool cumulative) { var clickedObject = blueprint.Item; - double rangeStart = clickedObject.StartTime; - double rangeEnd = clickedObject.GetEndTime(); - foreach (var selectedObject in SelectedItems) + Debug.Assert(pivot != null); + + double rangeStart = Math.Min(clickedObject.StartTime, pivot.StartTime); + double rangeEnd = Math.Max(clickedObject.GetEndTime(), pivot.GetEndTime()); + + var newSelection = new HashSet(EditorBeatmap.HitObjects.Where(obj => isInRange(obj, rangeStart, rangeEnd))); + + if (cumulative) { - rangeStart = Math.Min(rangeStart, selectedObject.StartTime); - rangeEnd = Math.Max(rangeEnd, selectedObject.GetEndTime()); + pivot = clickedObject; + newSelection.UnionWith(EditorBeatmap.SelectedHitObjects); } EditorBeatmap.SelectedHitObjects.Clear(); - EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects.Where(obj => isInRange(obj, rangeStart, rangeEnd))); + EditorBeatmap.SelectedHitObjects.AddRange(newSelection); bool isInRange(HitObject hitObject, double start, double end) => hitObject.StartTime >= start && hitObject.GetEndTime() <= end; From 81921bee118901a64b893fca036f217a4723fd45 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Sun, 26 Sep 2021 19:46:24 +0000 Subject: [PATCH 051/391] updated rhythmbonus to be OD sensitive --- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 01d50c1f0d..5706e9f2cb 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private const double pi_over_4 = Math.PI / 4; private const double pi_over_2 = Math.PI / 2; - private const double rhythm_multiplier = 0.5; + private const double rhythm_multiplier = 2.0; private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max. private double skillMultiplier => 1375; @@ -80,9 +80,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills double effectiveRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta); if (effectiveRatio > 0.5) - effectiveRatio = 0.5 + (effectiveRatio - 0.5) * 10; // large buff for 1/3 -> 1/4 type transitions. - - effectiveRatio *= Math.Max(prevDelta, currDelta) / greatWindowFull; // Increase scaling for when hitwindow is large but accuracy range is small. + effectiveRatio = 0.5 + (effectiveRatio - 0.5) * 5; // large buff for 1/3 -> 1/4 type transitions. effectiveRatio *= currHistoricalDecay; // scale with time @@ -130,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills } } - 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 * Math.Sqrt(52 / greatWindowFull)) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though) } private double tapStrainOf(DifficultyHitObject current, double speedBonus, double strainTime) From e4fb5a01c93ce43fc8fd3993b9a1adf50b00a965 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Sun, 26 Sep 2021 21:58:26 +0000 Subject: [PATCH 052/391] readded missing aim nerf for low acc --- 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 bf4d92652c..e936fa14d9 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -114,8 +114,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= approachRateBonus; - // Scale the aim value with accuracy _slightly_. - aimValue *= 0.5 + accuracy / 2.0; + // Scale the aim value with accuracy + aimValue *= accuracy; // It is important to also consider accuracy difficulty when doing that. aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500; From 7001578045e509ba12c06ca9e3afa50e60182cb2 Mon Sep 17 00:00:00 2001 From: Xexxar <38889014+Xexxar@users.noreply.github.com> Date: Sun, 26 Sep 2021 20:39:57 -0500 Subject: [PATCH 053/391] Update osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- .../Difficulty/Preprocessing/OsuDifficultyHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 52f18539c4..b9ba6364af 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing public double? Angle { get; private set; } /// - /// Milliseconds elapsed since the start time of the previous , with a minimum of 50ms. + /// Milliseconds elapsed since the start time of the previous , with a minimum of 25ms. /// public readonly double StrainTime; From df850924262e726c5b4c5d71a159314dad55e769 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Sep 2021 14:24:17 +0900 Subject: [PATCH 054/391] Resolve inner items early in process and rename variable --- .../HitObjectOrderedSelectionContainer.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs index e8286c5128..3fc26fa974 100644 --- a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs @@ -41,25 +41,25 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override int Compare(Drawable x, Drawable y) { - var xObj = (SelectionBlueprint)x; - var yObj = (SelectionBlueprint)y; + var xObj = ((SelectionBlueprint)x).Item; + var yObj = ((SelectionBlueprint)y).Item; // Put earlier blueprints towards the end of the list, so they handle input first - int i = yObj.Item.StartTime.CompareTo(xObj.Item.StartTime); - if (i != 0) return i; + int result = yObj.StartTime.CompareTo(xObj.StartTime); + if (result != 0) return result; // Fall back to end time if the start time is equal. - i = yObj.Item.GetEndTime().CompareTo(xObj.Item.GetEndTime()); - if (i != 0) return i; + result = yObj.GetEndTime().CompareTo(xObj.GetEndTime()); + if (result != 0) return result; // As a final fallback, use combo information if available. - if (xObj.Item is IHasComboInformation xHasCombo && yObj.Item is IHasComboInformation yHasCombo) + if (xObj is IHasComboInformation xHasCombo && yObj is IHasComboInformation yHasCombo) { - i = yHasCombo.ComboIndex.CompareTo(xHasCombo.ComboIndex); - if (i != 0) return i; + result = yHasCombo.ComboIndex.CompareTo(xHasCombo.ComboIndex); + if (result != 0) return result; - i = yHasCombo.IndexInCurrentCombo.CompareTo(xHasCombo.IndexInCurrentCombo); - if (i != 0) return i; + result = yHasCombo.IndexInCurrentCombo.CompareTo(xHasCombo.IndexInCurrentCombo); + if (result != 0) return result; } return CompareReverseChildID(y, x); From d4310f5d9a709b49f97f85c17a1afa7f6d80ecda Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Sep 2021 17:32:40 +0900 Subject: [PATCH 055/391] Move database connection string operations local In line with framework changes in https://github.com/ppy/osu-framework/pull/4793. --- .../Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs | 3 ++- osu.Game/Database/DatabaseContextFactory.cs | 8 +++++--- osu.Game/IO/WrappedStorage.cs | 5 ----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs index 6ae7f7481e..3dd34f6c2f 100644 --- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs @@ -13,6 +13,7 @@ using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Framework.Threading; +using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using SharpCompress.Compressors; @@ -160,7 +161,7 @@ namespace osu.Game.Beatmaps try { - using (var db = new SqliteConnection(storage.GetDatabaseConnectionString("online"))) + using (var db = new SqliteConnection(DatabaseContextFactory.CreateDatabaseConnectionString("online.db", storage))) { db.Open(); diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index d402195f29..94fa967d72 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.cs @@ -13,7 +13,7 @@ namespace osu.Game.Database { private readonly Storage storage; - private const string database_name = @"client"; + private const string database_name = @"client.db"; private ThreadLocal threadContexts; @@ -139,7 +139,7 @@ namespace osu.Game.Database threadContexts = new ThreadLocal(CreateContext, true); } - protected virtual OsuDbContext CreateContext() => new OsuDbContext(storage.GetDatabaseConnectionString(database_name)) + protected virtual OsuDbContext CreateContext() => new OsuDbContext(CreateDatabaseConnectionString(database_name, storage)) { Database = { AutoTransactionsEnabled = false } }; @@ -152,7 +152,7 @@ namespace osu.Game.Database try { - storage.DeleteDatabase(database_name); + storage.Delete(database_name); } catch { @@ -171,5 +171,7 @@ namespace osu.Game.Database recycleThreadContexts(); } + + public static string CreateDatabaseConnectionString(string filename, Storage storage) => string.Concat("Data Source=", storage.GetFullPath($@"{filename}", true)); } } diff --git a/osu.Game/IO/WrappedStorage.cs b/osu.Game/IO/WrappedStorage.cs index 5b2549d2ee..b9ccc907d9 100644 --- a/osu.Game/IO/WrappedStorage.cs +++ b/osu.Game/IO/WrappedStorage.cs @@ -70,11 +70,6 @@ namespace osu.Game.IO public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) => UnderlyingStorage.GetStream(MutatePath(path), access, mode); - public override string GetDatabaseConnectionString(string name) => - UnderlyingStorage.GetDatabaseConnectionString(MutatePath(name)); - - public override void DeleteDatabase(string name) => UnderlyingStorage.DeleteDatabase(MutatePath(name)); - public override void OpenPathInNativeExplorer(string path) => UnderlyingStorage.OpenPathInNativeExplorer(MutatePath(path)); public override Storage GetStorageForDirectory(string path) From bcdbffbca0168c295f313691ccdde3b0b250c5dc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Sep 2021 20:56:54 +0900 Subject: [PATCH 056/391] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 967405cd2e..27df6cf296 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 61ae6aa69e..363bf7f517 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index e032926a9c..4b8b47f982 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From 1a1fc00b44198cb0e2cfd26b36a9c935f07e01fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Sep 2021 20:43:08 +0200 Subject: [PATCH 057/391] Add failing test case --- .../Editing/TestSceneTimelineSelection.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs index f4703b79f0..2544b6c2a1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineSelection.cs @@ -223,6 +223,41 @@ namespace osu.Game.Tests.Visual.Editing moveMouseToObject(() => addedObjects[2]); AddStep("click first", () => InputManager.Click(MouseButton.Left)); assertSelectionIs(addedObjects.Take(3)); + + AddStep("release keys", () => + { + InputManager.ReleaseKey(Key.ControlLeft); + InputManager.ReleaseKey(Key.ShiftLeft); + }); + } + + [Test] + public void TestRangeSelectAfterExternalSelection() + { + var addedObjects = new[] + { + new HitCircle { StartTime = 100 }, + new HitCircle { StartTime = 200, Position = new Vector2(100) }, + new HitCircle { StartTime = 300, Position = new Vector2(200) }, + new HitCircle { StartTime = 400, Position = new Vector2(300) }, + }; + + AddStep("add hitobjects", () => EditorBeatmap.AddRange(addedObjects)); + + AddStep("select all without mouse", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects)); + assertSelectionIs(addedObjects); + + AddStep("hold down shift", () => InputManager.PressKey(Key.ShiftLeft)); + + moveMouseToObject(() => addedObjects[1]); + AddStep("click second object", () => InputManager.Click(MouseButton.Left)); + assertSelectionIs(addedObjects); + + moveMouseToObject(() => addedObjects[3]); + AddStep("click fourth object", () => InputManager.Click(MouseButton.Left)); + assertSelectionIs(addedObjects.Skip(1)); + + AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); } private void assertSelectionIs(IEnumerable hitObjects) From ca6cbca04a1bbbd14536110e068f79c2f06537e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Sep 2021 20:54:01 +0200 Subject: [PATCH 058/391] Fix range selection crashing after non-mouse selection --- .../Compose/Components/Timeline/TimelineSelectionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineSelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineSelectionHandler.cs index ad770b40b5..845a671e2c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineSelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineSelectionHandler.cs @@ -80,7 +80,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline internal override bool MouseDownSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) { - if (e.ShiftPressed && e.Button == MouseButton.Left && SelectedItems.Any()) + if (e.ShiftPressed && e.Button == MouseButton.Left && pivot != null) { handleRangeSelection(blueprint, e.ControlPressed); return true; From 7a0499ad07bc19449981cbb5c4983934deda8658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Sep 2021 21:45:26 +0200 Subject: [PATCH 059/391] Fix repeat arrow texture not falling back to default legacy skin --- .../Skinning/Legacy/LegacyReverseArrow.cs | 12 +++--------- .../Skinning/Legacy/OsuLegacySkinTransformer.cs | 2 +- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs index fd7bfe7e60..f605b36122 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs @@ -13,26 +13,20 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public class LegacyReverseArrow : CompositeDrawable { - private ISkin skin { get; } - [Resolved(canBeNull: true)] private DrawableHitObject drawableHitObject { get; set; } private Drawable proxy; - public LegacyReverseArrow(ISkin skin) - { - this.skin = skin; - } - [BackgroundDependencyLoader] - private void load() + private void load(ISkinSource skinSource) { AutoSizeAxes = Axes.Both; string lookupName = new OsuSkinComponent(OsuSkinComponents.ReverseArrow).LookupName; - InternalChild = skin.GetAnimation(lookupName, true, true) ?? Empty(); + var skin = skinSource.FindProvider(skin => skin.GetTexture(lookupName) != null); + InternalChild = skin?.GetAnimation(lookupName, true, true) ?? Empty(); } protected override void LoadComplete() diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 8a24e36420..ff9f6f0e07 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy case OsuSkinComponents.ReverseArrow: if (hasHitCircle.Value) - return new LegacyReverseArrow(this); + return new LegacyReverseArrow(); return null; From 5bd09a4a30ec343a7aa78224522041a8121021ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 27 Sep 2021 22:21:14 +0200 Subject: [PATCH 060/391] Rename inner lambda parameter --- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs index f605b36122..7a071b5a03 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyReverseArrow.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy string lookupName = new OsuSkinComponent(OsuSkinComponents.ReverseArrow).LookupName; - var skin = skinSource.FindProvider(skin => skin.GetTexture(lookupName) != null); + var skin = skinSource.FindProvider(s => s.GetTexture(lookupName) != null); InternalChild = skin?.GetAnimation(lookupName, true, true) ?? Empty(); } From f148fbcc94774816b607a33307e7627279e12631 Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Wed, 29 Sep 2021 00:59:08 +0200 Subject: [PATCH 061/391] Cap LoopCount to at least 1 --- osu.Game/Storyboards/CommandLoop.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/CommandLoop.cs b/osu.Game/Storyboards/CommandLoop.cs index c22ca0d8c0..c17436d813 100644 --- a/osu.Game/Storyboards/CommandLoop.cs +++ b/osu.Game/Storyboards/CommandLoop.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; namespace osu.Game.Storyboards @@ -16,7 +17,7 @@ namespace osu.Game.Storyboards public CommandLoop(double startTime, int loopCount) { LoopStartTime = startTime; - LoopCount = loopCount; + LoopCount = Math.Max(1, loopCount); } public override IEnumerable.TypedCommand> GetCommands(CommandTimelineSelector timelineSelector, double offset = 0) From 117d47bf7fab71653dc7cc35b469486a3a8df43a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Sep 2021 11:26:37 +0900 Subject: [PATCH 062/391] Rename JSON key resolver to better expose what it's doing --- osu.Game/IO/Serialization/JsonSerializableExtensions.cs | 2 +- .../{KeyContractResolver.cs => SnakeCaseKeyContractResolver.cs} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename osu.Game/IO/Serialization/{KeyContractResolver.cs => SnakeCaseKeyContractResolver.cs} (84%) diff --git a/osu.Game/IO/Serialization/JsonSerializableExtensions.cs b/osu.Game/IO/Serialization/JsonSerializableExtensions.cs index 5b47d0bad1..68b2110ede 100644 --- a/osu.Game/IO/Serialization/JsonSerializableExtensions.cs +++ b/osu.Game/IO/Serialization/JsonSerializableExtensions.cs @@ -22,7 +22,7 @@ namespace osu.Game.IO.Serialization ObjectCreationHandling = ObjectCreationHandling.Replace, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate, Converters = new List { new Vector2Converter() }, - ContractResolver = new KeyContractResolver() + ContractResolver = new SnakeCaseKeyContractResolver() }; } } diff --git a/osu.Game/IO/Serialization/KeyContractResolver.cs b/osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.cs similarity index 84% rename from osu.Game/IO/Serialization/KeyContractResolver.cs rename to osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.cs index 7b6905643c..68cb66d0dc 100644 --- a/osu.Game/IO/Serialization/KeyContractResolver.cs +++ b/osu.Game/IO/Serialization/SnakeCaseKeyContractResolver.cs @@ -6,7 +6,7 @@ using Newtonsoft.Json.Serialization; namespace osu.Game.IO.Serialization { - public class KeyContractResolver : DefaultContractResolver + public class SnakeCaseKeyContractResolver : DefaultContractResolver { protected override string ResolvePropertyName(string propertyName) { From dcf87e97c282ce1d3580087a3d900b905bfe0a95 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Sep 2021 14:03:41 +0900 Subject: [PATCH 063/391] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 27df6cf296..8fad10d247 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 363bf7f517..ba118c5240 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 4b8b47f982..37931d0c38 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From c5b7e97bd9bde610074c8f529d30f347083a8003 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Sep 2021 18:50:55 +0900 Subject: [PATCH 064/391] Fix skin editor potentially crashing during close process As reported at https://github.com/ppy/osu/discussions/14850#discussioncomment-1399382. --- osu.Game/Skinning/Editor/SkinEditorOverlay.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs index a892ec1ca3..2b25ccbe87 100644 --- a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs +++ b/osu.Game/Skinning/Editor/SkinEditorOverlay.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.Diagnostics; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -20,6 +22,8 @@ namespace osu.Game.Skinning.Editor public class SkinEditorOverlay : CompositeDrawable, IKeyBindingHandler { private readonly ScalingContainer target; + + [CanBeNull] private SkinEditor skinEditor; public const float VISIBLE_TARGET_SCALE = 0.8f; @@ -62,6 +66,8 @@ namespace osu.Game.Skinning.Editor public override void Hide() { + Debug.Assert(skinEditor != null); + // base call intentionally omitted. skinEditor.Hide(); } @@ -71,8 +77,12 @@ namespace osu.Game.Skinning.Editor // base call intentionally omitted. if (skinEditor == null) { - LoadComponentAsync(skinEditor = new SkinEditor(target), AddInternal); + skinEditor = new SkinEditor(target); skinEditor.State.BindValueChanged(editorVisibilityChanged); + + Debug.Assert(skinEditor != null); + + LoadComponentAsync(skinEditor, AddInternal); } else skinEditor.Show(); @@ -98,8 +108,13 @@ namespace osu.Game.Skinning.Editor } } - private void updateMasking() => + private void updateMasking() + { + if (skinEditor == null) + return; + target.Masking = skinEditor.State.Value == Visibility.Visible; + } public void OnReleased(KeyBindingReleaseEvent e) { From 4fd19cbb0d54e664e644d0a65c968acea9eb3760 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Sep 2021 18:58:43 +0900 Subject: [PATCH 065/391] Use null check instead of assert in `Hide()` due to public exposure --- osu.Game/Skinning/Editor/SkinEditorOverlay.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs index 2b25ccbe87..d27122aea8 100644 --- a/osu.Game/Skinning/Editor/SkinEditorOverlay.cs +++ b/osu.Game/Skinning/Editor/SkinEditorOverlay.cs @@ -66,10 +66,8 @@ namespace osu.Game.Skinning.Editor public override void Hide() { - Debug.Assert(skinEditor != null); - // base call intentionally omitted. - skinEditor.Hide(); + skinEditor?.Hide(); } public override void Show() From 7914daaef0731ad86c63716f8df5d0b1e338617a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Sep 2021 19:09:48 +0900 Subject: [PATCH 066/391] Remove unused using --- osu.Game/OsuGame.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index dde60158d1..99925bb1fb 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -22,7 +22,6 @@ using Humanizer; using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Bindables; -using osu.Framework.Development; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; From 73ee82ee2b95fcde64fe18168d3f705efe089414 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Sep 2021 19:15:58 +0900 Subject: [PATCH 067/391] Rename RecentParticipantsList -> DrawableRoomParticipantsList --- .../Multiplayer/TestSceneDrawableRoom.cs | 2 +- ... TestSceneDrawableRoomParticipantsList.cs} | 30 +++++++++---------- .../Lounge/Components/DrawableRoom.cs | 8 ++--- ...ist.cs => DrawableRoomParticipantsList.cs} | 4 +-- 4 files changed, 22 insertions(+), 22 deletions(-) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneRecentParticipantsList.cs => TestSceneDrawableRoomParticipantsList.cs} (82%) rename osu.Game/Screens/OnlinePlay/Lounge/Components/{RecentParticipantsList.cs => DrawableRoomParticipantsList.cs} (98%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs index b1f5781f6f..0d4b14f90b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Type = { Value = MatchType.HeadToHead }, })); - AddUntilStep("wait for panel load", () => drawableRoom.ChildrenOfType().Any()); + AddUntilStep("wait for panel load", () => drawableRoom.ChildrenOfType().Any()); AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha)); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRecentParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs similarity index 82% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneRecentParticipantsList.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs index 50ec2bf3ac..ea75898946 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRecentParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs @@ -13,16 +13,16 @@ using osu.Game.Users.Drawables; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneRecentParticipantsList : OnlinePlayTestScene + public class TestSceneDrawableRoomParticipantsList : OnlinePlayTestScene { - private RecentParticipantsList list; + private DrawableRoomParticipantsList list; [SetUp] public new void Setup() => Schedule(() => { SelectedRoom.Value = new Room { Name = { Value = "test room" } }; - Child = list = new RecentParticipantsList + Child = list = new DrawableRoomParticipantsList { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -40,19 +40,19 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddStep("set 8 circles", () => list.NumberOfCircles = 8); - AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); AddStep("add one more user", () => addUser(9)); - AddAssert("2 hidden users", () => list.ChildrenOfType().Single().Count == 2); + AddAssert("2 hidden users", () => list.ChildrenOfType().Single().Count == 2); AddStep("remove first user", () => removeUserAt(0)); - AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); AddStep("add one more user", () => addUser(9)); - AddAssert("2 hidden users", () => list.ChildrenOfType().Single().Count == 2); + AddAssert("2 hidden users", () => list.ChildrenOfType().Single().Count == 2); AddStep("remove last user", () => removeUserAt(8)); - AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); } [Test] @@ -87,11 +87,11 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("set 3 circles", () => list.NumberOfCircles = 3); AddAssert("2 users displayed", () => list.ChildrenOfType().Count() == 2); - AddAssert("48 hidden users", () => list.ChildrenOfType().Single().Count == 48); + AddAssert("48 hidden users", () => list.ChildrenOfType().Single().Count == 48); AddStep("set 10 circles", () => list.NumberOfCircles = 10); AddAssert("9 users displayed", () => list.ChildrenOfType().Count() == 9); - AddAssert("41 hidden users", () => list.ChildrenOfType().Single().Count == 41); + AddAssert("41 hidden users", () => list.ChildrenOfType().Single().Count == 41); } [Test] @@ -105,20 +105,20 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("remove from start", () => removeUserAt(0)); AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); - AddAssert("46 hidden users", () => list.ChildrenOfType().Single().Count == 46); + AddAssert("46 hidden users", () => list.ChildrenOfType().Single().Count == 46); AddStep("remove from end", () => removeUserAt(SelectedRoom.Value.RecentParticipants.Count - 1)); AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); - AddAssert("45 hidden users", () => list.ChildrenOfType().Single().Count == 45); + AddAssert("45 hidden users", () => list.ChildrenOfType().Single().Count == 45); AddRepeatStep("remove 45 users", () => removeUserAt(0), 45); AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); - AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); - AddAssert("hidden users bubble hidden", () => list.ChildrenOfType().Single().Alpha < 0.5f); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + AddAssert("hidden users bubble hidden", () => list.ChildrenOfType().Single().Alpha < 0.5f); AddStep("remove another user", () => removeUserAt(0)); AddAssert("2 circles displayed", () => list.ChildrenOfType().Count() == 2); - AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); + AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); AddRepeatStep("remove the remaining two users", () => removeUserAt(0), 2); AddAssert("0 circles displayed", () => !list.ChildrenOfType().Any()); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 80070aa6ba..f2cd9d1410 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private readonly Bindable roomCategory = new Bindable(); private readonly Bindable hasPassword = new Bindable(); - private RecentParticipantsList recentParticipantsList; + private DrawableRoomParticipantsList drawableRoomParticipantsList; private RoomSpecialCategoryPill specialCategoryPill; private PasswordProtectedIcon passwordIcon; private EndDateInfo endDateInfo; @@ -217,7 +217,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Children = new Drawable[] { ButtonsContainer, - recentParticipantsList = new RecentParticipantsList + drawableRoomParticipantsList = new DrawableRoomParticipantsList { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, @@ -280,8 +280,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { numberOfAvatars = value; - if (recentParticipantsList != null) - recentParticipantsList.NumberOfCircles = value; + if (drawableRoomParticipantsList != null) + drawableRoomParticipantsList.NumberOfCircles = value; } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RecentParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs similarity index 98% rename from osu.Game/Screens/OnlinePlay/Lounge/Components/RecentParticipantsList.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index bc658f45e4..961ab276dc 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RecentParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -17,7 +17,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public class RecentParticipantsList : OnlinePlayComposite + public class DrawableRoomParticipantsList : OnlinePlayComposite { private const float avatar_size = 36; @@ -26,7 +26,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private HiddenUserCount hiddenUsers; private OsuSpriteText totalCount; - public RecentParticipantsList() + public DrawableRoomParticipantsList() { AutoSizeAxes = Axes.X; Height = 60; From d89577b2e7d3056631b082418be5488775363183 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Sep 2021 19:34:40 +0900 Subject: [PATCH 068/391] Add host to DrawableRoomParticipantsList --- .../TestSceneDrawableRoomParticipantsList.cs | 13 +- .../DrawableRoomParticipantsList.cs | 133 ++++++++++++++---- 2 files changed, 115 insertions(+), 31 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs index ea75898946..6909bc3f97 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs @@ -20,7 +20,18 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUp] public new void Setup() => Schedule(() => { - SelectedRoom.Value = new Room { Name = { Value = "test room" } }; + SelectedRoom.Value = new Room + { + Name = { Value = "test room" }, + Host = + { + Value = new User + { + Id = 2, + Username = "peppy", + } + } + }; Child = list = new DrawableRoomParticipantsList { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index 961ab276dc..3d366a4c92 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -4,11 +4,13 @@ using System.Collections.Specialized; 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.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Users; @@ -23,6 +25,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private FillFlowContainer avatarFlow; + private CircularAvatar hostAvatar; + private LinkFlowContainer hostText; private HiddenUserCount hiddenUsers; private OsuSpriteText totalCount; @@ -51,42 +55,99 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components }, new FillFlowContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(4), - Padding = new MarginPadding { Right = 16 }, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, Children = new Drawable[] { - new SpriteIcon + new FillFlowContainer { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(16), - Margin = new MarginPadding { Left = 8 }, - Icon = FontAwesome.Solid.User, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Spacing = new Vector2(8), + Padding = new MarginPadding + { + Left = 8, + Right = 16 + }, + Children = new Drawable[] + { + hostAvatar = new CircularAvatar + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + hostText = new LinkFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Text = "hosted by smoogipoo" + } + } }, - totalCount = new OsuSpriteText + new Container { - Font = OsuFont.Default.With(weight: FontWeight.Bold), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + Shear = new Vector2(0.2f, 0), + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Background3, + } + }, + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Padding = new MarginPadding + { + Left = 8, + Right = 16 + }, + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(16), + Icon = FontAwesome.Solid.User, + }, + totalCount = new OsuSpriteText + { + Font = OsuFont.Default.With(weight: FontWeight.Bold), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + avatarFlow = new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4), + Margin = new MarginPadding { Left = 4 }, + }, + hiddenUsers = new HiddenUserCount + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } + } + } }, - avatarFlow = new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(4), - Margin = new MarginPadding { Left = 4 }, - }, - hiddenUsers = new HiddenUserCount - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - } } } }; @@ -102,6 +163,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components updateHiddenUsers(); totalCount.Text = ParticipantCount.Value.ToString(); }, true); + + Host.BindValueChanged(onHostChanged, true); } private int numberOfCircles = 4; @@ -194,6 +257,16 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } + private void onHostChanged(ValueChangedEvent host) + { + hostAvatar.User = host.NewValue; + hostText.Clear(); + + hostText.AddText(@"hosted by "); + if (host.NewValue != null) + hostText.AddUserLink(host.NewValue); + } + private class CircularAvatar : CompositeDrawable { public User User From 5f921c7836d8be698a3d3f66bc67a27251c36cd3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Sep 2021 20:24:32 +0900 Subject: [PATCH 069/391] Change SelectedItem to show the last item by default --- osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index 24b3b4ec94..722a2a0b94 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.OnlinePlay protected Bindable Duration { get; private set; } /// - /// The currently selected item in the , or the first item from + /// The currently selected item in the , or the last item from /// if this is not within a . /// protected readonly Bindable SelectedItem = new Bindable(); @@ -80,7 +80,7 @@ namespace osu.Game.Screens.OnlinePlay protected virtual void UpdateSelectedItem() { - SelectedItem.Value = Playlist.FirstOrDefault(); + SelectedItem.Value = Playlist.LastOrDefault(); } } } From 67d847fbd3493c30fb1821345e38dfec4299487c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Sep 2021 20:24:49 +0900 Subject: [PATCH 070/391] Add room status text to DrawableRoom --- .../Multiplayer/TestSceneDrawableRoom.cs | 21 ++++----- .../Lounge/Components/DrawableRoom.cs | 43 +++++++++++++------ 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs index 0d4b14f90b..22ff2b98ce 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoom.cs @@ -43,11 +43,12 @@ namespace osu.Game.Tests.Visual.Multiplayer Spacing = new Vector2(10), Children = new Drawable[] { - createDrawableRoom(new Room + createLoungeRoom(new Room { - Name = { Value = "Flyte's Trash Playlist" }, + Name = { Value = "Multiplayer room" }, Status = { Value = new RoomStatusOpen() }, EndDate = { Value = DateTimeOffset.Now.AddDays(1) }, + Type = { Value = MatchType.HeadToHead }, Playlist = { new PlaylistItem @@ -65,9 +66,9 @@ namespace osu.Game.Tests.Visual.Multiplayer } } }), - createDrawableRoom(new Room + createLoungeRoom(new Room { - Name = { Value = "Room 2" }, + Name = { Value = "Playlist room with multiple beatmaps" }, Status = { Value = new RoomStatusPlaying() }, EndDate = { Value = DateTimeOffset.Now.AddDays(1) }, Playlist = @@ -100,15 +101,15 @@ namespace osu.Game.Tests.Visual.Multiplayer } } }), - createDrawableRoom(new Room + createLoungeRoom(new Room { - Name = { Value = "Room 3" }, + Name = { Value = "Finished room" }, Status = { Value = new RoomStatusEnded() }, EndDate = { Value = DateTimeOffset.Now }, }), - createDrawableRoom(new Room + createLoungeRoom(new Room { - Name = { Value = "Room 4 (spotlight)" }, + Name = { Value = "Spotlight room" }, Status = { Value = new RoomStatusOpen() }, Category = { Value = RoomCategory.Spotlight }, }), @@ -123,7 +124,7 @@ namespace osu.Game.Tests.Visual.Multiplayer DrawableRoom drawableRoom = null; Room room = null; - AddStep("create room", () => Child = drawableRoom = createDrawableRoom(room = new Room + AddStep("create room", () => Child = drawableRoom = createLoungeRoom(room = new Room { Name = { Value = "Room with password" }, Status = { Value = new RoomStatusOpen() }, @@ -141,7 +142,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType().Single().Alpha)); } - private DrawableRoom createDrawableRoom(Room room) + private DrawableRoom createLoungeRoom(Room room) { room.Host.Value ??= new User { Username = "peppy", Id = 2 }; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index f2cd9d1410..df356e64bd 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -14,6 +14,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.OnlinePlay.Components; @@ -172,7 +173,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Children = new Drawable[] { new RoomNameText(), - new RoomHostText(), + new RoomStatusText() } } }, @@ -304,11 +305,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } - private class RoomHostText : OnlinePlayComposite + private class RoomStatusText : OnlinePlayComposite { - private LinkFlowContainer hostText; + [Resolved] + private OsuColour colours { get; set; } - public RoomHostText() + private LinkFlowContainer linkFlow; + + public RoomStatusText() { AutoSizeAxes = Axes.Both; } @@ -316,26 +320,37 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components [BackgroundDependencyLoader] private void load() { - InternalChild = hostText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 16)) + InternalChild = linkFlow = new LinkFlowContainer(s => { - AutoSizeAxes = Axes.Both + s.Font = OsuFont.Default.With(size: 16); + s.Colour = colours.Lime1; + }) + { + AutoSizeAxes = Axes.Both, }; } protected override void LoadComplete() { base.LoadComplete(); + SelectedItem.BindValueChanged(onSelectedItemChanged, true); + } - Host.BindValueChanged(host => + private void onSelectedItemChanged(ValueChangedEvent item) + { + if (Type.Value == MatchType.Playlists) { - hostText.Clear(); + linkFlow.Text = "Waiting for players"; + return; + } - if (host.NewValue != null) - { - hostText.AddText("hosted by "); - hostText.AddUserLink(host.NewValue); - } - }, true); + linkFlow.Clear(); + + if (item.NewValue?.Beatmap.Value != null) + { + linkFlow.AddText("Currently playing "); + linkFlow.AddLink(item.NewValue.Beatmap.Value.ToRomanisableString(), LinkAction.OpenBeatmap, item.NewValue.Beatmap.Value.OnlineBeatmapID.ToString()); + } } } From 94e2dbd7e76f4da32b6be693d7d17ae740ca83b5 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 23 Sep 2021 20:14:32 +0900 Subject: [PATCH 071/391] Add a low-pass filter effect to music when certain popup dialogs are shown --- osu.Game/Audio/Effects/LowPassFilter.cs | 75 +++++++++++++++++++++++++ osu.Game/Overlays/DialogOverlay.cs | 15 +++++ 2 files changed, 90 insertions(+) create mode 100644 osu.Game/Audio/Effects/LowPassFilter.cs diff --git a/osu.Game/Audio/Effects/LowPassFilter.cs b/osu.Game/Audio/Effects/LowPassFilter.cs new file mode 100644 index 0000000000..b1e78e4324 --- /dev/null +++ b/osu.Game/Audio/Effects/LowPassFilter.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 ManagedBass.Fx; +using osu.Framework.Audio.Mixing; +using osu.Framework.Bindables; +using osu.Framework.Graphics; + +namespace osu.Game.Audio.Effects +{ + public class LowPassFilter : Component + { + private const float filter_cutoff_start = 2000; + private const float filter_cutoff_end = 150; + private const float filter_sweep_duration = 100; + private readonly Bindable filterFreq = new Bindable(filter_cutoff_start); + private readonly AudioMixer mixer; + private readonly BQFParameters filter; + + /// + /// A toggle-able low-pass filter with a subtle filter-sweep effect when toggled that can be attached to an . + /// + public LowPassFilter(AudioMixer mixer) + { + this.mixer = mixer; + filter = new BQFParameters + { + lFilter = BQFType.LowPass, + fCenter = filterFreq.Value + }; + } + + public void Enable() + { + attachFilter(); + this.TransformBindableTo(filterFreq, filter_cutoff_end, filter_sweep_duration); + } + + public void Disable() + { + this.TransformBindableTo(filterFreq, filter_cutoff_start, filter_sweep_duration) + .OnComplete(_ => detatchFilter()); + } + + private void attachFilter() + { + mixer.Effects.Add(filter); + filterFreq.ValueChanged += updateFilter; + } + + private void detatchFilter() + { + filterFreq.ValueChanged -= updateFilter; + mixer.Effects.Remove(filter); + } + + private void updateFilter(ValueChangedEvent cutoff) + { + var filterIndex = mixer.Effects.IndexOf(filter); + if (filterIndex < 0) return; + + var existingFilter = mixer.Effects[filterIndex] as BQFParameters; + if (existingFilter == null) return; + + existingFilter.fCenter = cutoff.NewValue; + mixer.Effects[filterIndex] = existingFilter; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + detatchFilter(); + } + } +} diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index f051e09c08..d653ef3888 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -7,7 +7,10 @@ using osu.Game.Overlays.Dialog; 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 { @@ -18,6 +21,8 @@ namespace osu.Game.Overlays protected override string PopInSampleName => "UI/dialog-pop-in"; protected override string PopOutSampleName => "UI/dialog-pop-out"; + private LowPassFilter filter; + public PopupDialog CurrentDialog { get; private set; } public DialogOverlay() @@ -34,6 +39,12 @@ namespace osu.Game.Overlays Origin = Anchor.BottomCentre; } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + AddInternal(filter = new LowPassFilter(audio.TrackMixer)); + } + public void Push(PopupDialog dialog) { if (dialog == CurrentDialog || dialog.State.Value != Visibility.Visible) return; @@ -71,12 +82,16 @@ namespace osu.Game.Overlays { base.PopIn(); this.FadeIn(PopupDialog.ENTER_DURATION, Easing.OutQuint); + filter.Enable(); } protected override void PopOut() { base.PopOut(); + if (IsLoaded) + filter.Disable(); + if (CurrentDialog?.State.Value == Visibility.Visible) { CurrentDialog.Hide(); From 2608d193a9518e2403cc3c1053a93e5149a3fb5d Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 29 Sep 2021 19:19:33 +0900 Subject: [PATCH 072/391] Refactor `Filter` component to be more re-usable --- .../Effects/{LowPassFilter.cs => Filter.cs} | 44 ++++++++++++------- osu.Game/Overlays/DialogOverlay.cs | 4 +- 2 files changed, 30 insertions(+), 18 deletions(-) rename osu.Game/Audio/Effects/{LowPassFilter.cs => Filter.cs} (60%) diff --git a/osu.Game/Audio/Effects/LowPassFilter.cs b/osu.Game/Audio/Effects/Filter.cs similarity index 60% rename from osu.Game/Audio/Effects/LowPassFilter.cs rename to osu.Game/Audio/Effects/Filter.cs index b1e78e4324..37a2fa93db 100644 --- a/osu.Game/Audio/Effects/LowPassFilter.cs +++ b/osu.Game/Audio/Effects/Filter.cs @@ -8,50 +8,62 @@ using osu.Framework.Graphics; namespace osu.Game.Audio.Effects { - public class LowPassFilter : Component + public class Filter : Component { - private const float filter_cutoff_start = 2000; - private const float filter_cutoff_end = 150; - private const float filter_sweep_duration = 100; - private readonly Bindable filterFreq = new Bindable(filter_cutoff_start); + public BQFType FilterType = BQFType.LowPass; + public float SweepCutoffStart = 2000; + public float SweepCutoffEnd = 150; + public float SweepDuration = 100; + public Easing SweepEasing = Easing.None; + + public bool IsActive { get; private set; } + + private readonly Bindable filterFreq = new Bindable(); private readonly AudioMixer mixer; - private readonly BQFParameters filter; + private BQFParameters filter; /// - /// A toggle-able low-pass filter with a subtle filter-sweep effect when toggled that can be attached to an . + /// A BiQuad filter that performs a filter-sweep when toggled on or off. /// - public LowPassFilter(AudioMixer mixer) + /// The mixer this effect should be attached to. + public Filter(AudioMixer mixer) { this.mixer = mixer; - filter = new BQFParameters - { - lFilter = BQFType.LowPass, - fCenter = filterFreq.Value - }; } public void Enable() { attachFilter(); - this.TransformBindableTo(filterFreq, filter_cutoff_end, filter_sweep_duration); + this.TransformBindableTo(filterFreq, SweepCutoffEnd, SweepDuration, SweepEasing); } public void Disable() { - this.TransformBindableTo(filterFreq, filter_cutoff_start, filter_sweep_duration) - .OnComplete(_ => detatchFilter()); + this.TransformBindableTo(filterFreq, SweepCutoffStart, SweepDuration, SweepEasing).OnComplete(_ => detatchFilter()); } private void attachFilter() { + if (IsActive) return; + + filter = new BQFParameters + { + lFilter = FilterType, + fCenter = filterFreq.Value = SweepCutoffStart + }; + mixer.Effects.Add(filter); filterFreq.ValueChanged += updateFilter; + IsActive = true; } private void detatchFilter() { + if (!IsActive) return; + filterFreq.ValueChanged -= updateFilter; mixer.Effects.Remove(filter); + IsActive = false; } private void updateFilter(ValueChangedEvent cutoff) diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index d653ef3888..3fade81cec 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays protected override string PopInSampleName => "UI/dialog-pop-in"; protected override string PopOutSampleName => "UI/dialog-pop-out"; - private LowPassFilter filter; + private Filter filter; public PopupDialog CurrentDialog { get; private set; } @@ -42,7 +42,7 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load(AudioManager audio) { - AddInternal(filter = new LowPassFilter(audio.TrackMixer)); + AddInternal(filter = new Filter(audio.TrackMixer)); } public void Push(PopupDialog dialog) From c9c2d205447b5ddd5ade0787da36f88b79c20f5e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Sep 2021 20:44:38 +0900 Subject: [PATCH 073/391] Limit max size --- .../Lounge/Components/DrawableRoom.cs | 63 ++++++++++++++----- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index df356e64bd..9e693b0b0c 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -137,7 +137,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { new FillFlowContainer { - AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Children = new Drawable[] { @@ -167,7 +168,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components }, new FillFlowContainer { - AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Top = 3 }, Direction = FillDirection.Vertical, Children = new Drawable[] @@ -310,23 +312,47 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components [Resolved] private OsuColour colours { get; set; } - private LinkFlowContainer linkFlow; + private SpriteText statusText; + private LinkFlowContainer beatmapText; public RoomStatusText() { - AutoSizeAxes = Axes.Both; + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Width = 0.5f; } [BackgroundDependencyLoader] private void load() { - InternalChild = linkFlow = new LinkFlowContainer(s => + InternalChild = new GridContainer { - s.Font = OsuFont.Default.With(size: 16); - s.Colour = colours.Lime1; - }) - { - AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + statusText = new OsuSpriteText + { + Font = OsuFont.Default.With(size: 16), + Colour = colours.Lime1 + }, + beatmapText = new LinkFlowContainer(s => + { + s.Font = OsuFont.Default.With(size: 16); + s.Colour = colours.Lime1; + }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + } }; } @@ -338,18 +364,25 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private void onSelectedItemChanged(ValueChangedEvent item) { + beatmapText.Clear(); + if (Type.Value == MatchType.Playlists) { - linkFlow.Text = "Waiting for players"; + statusText.Text = "Waiting for players"; return; } - linkFlow.Clear(); - if (item.NewValue?.Beatmap.Value != null) { - linkFlow.AddText("Currently playing "); - linkFlow.AddLink(item.NewValue.Beatmap.Value.ToRomanisableString(), LinkAction.OpenBeatmap, item.NewValue.Beatmap.Value.OnlineBeatmapID.ToString()); + statusText.Text = "Currently playing "; + beatmapText.AddLink(item.NewValue.Beatmap.Value.ToRomanisableString(), + LinkAction.OpenBeatmap, + item.NewValue.Beatmap.Value.OnlineBeatmapID.ToString(), + creationParameters: s => + { + s.Truncate = true; + s.RelativeSizeAxes = Axes.X; + }); } } } From 909f8d1d9800eb7ffd820463865b38ea4e6f5264 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 29 Sep 2021 19:20:05 +0900 Subject: [PATCH 074/391] Add visual tests for `Filter` --- osu.Game.Tests/Audio/TestSceneFilter.cs | 95 +++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 osu.Game.Tests/Audio/TestSceneFilter.cs diff --git a/osu.Game.Tests/Audio/TestSceneFilter.cs b/osu.Game.Tests/Audio/TestSceneFilter.cs new file mode 100644 index 0000000000..fd4fe530eb --- /dev/null +++ b/osu.Game.Tests/Audio/TestSceneFilter.cs @@ -0,0 +1,95 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using ManagedBass.Fx; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Graphics; +using osu.Game.Audio.Effects; +using osu.Game.Beatmaps; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.Audio +{ + public class TestSceneFilter : OsuTestScene + { + [Resolved] + private AudioManager audio { get; set; } + + private WorkingBeatmap testBeatmap; + private Filter lowPassFilter; + private Filter highPassFilter; + private Filter bandPassFilter; + + [BackgroundDependencyLoader] + private void load() + { + testBeatmap = new WaveformTestBeatmap(audio); + AddRange(new Drawable[] + { + lowPassFilter = new Filter(audio.TrackMixer) + { + FilterType = BQFType.LowPass, + SweepCutoffStart = 2000, + SweepCutoffEnd = 150, + SweepDuration = 1000 + }, + highPassFilter = new Filter(audio.TrackMixer) + { + FilterType = BQFType.HighPass, + SweepCutoffStart = 150, + SweepCutoffEnd = 2000, + SweepDuration = 1000 + }, + bandPassFilter = new Filter(audio.TrackMixer) + { + FilterType = BQFType.BandPass, + SweepCutoffStart = 150, + SweepCutoffEnd = 20000, + SweepDuration = 1000 + }, + }); + } + + [Test] + public void TestLowPass() + { + testFilter(lowPassFilter); + } + + [Test] + public void TestHighPass() + { + testFilter(highPassFilter); + } + + [Test] + public void TestBandPass() + { + testFilter(bandPassFilter); + } + + private void testFilter(Filter filter) + { + AddStep("Prepare Track", () => + { + testBeatmap = new WaveformTestBeatmap(audio); + testBeatmap.LoadTrack(); + }); + AddStep("Play Track", () => + { + testBeatmap.Track.Start(); + }); + AddWaitStep("Let track play", 10); + AddStep("Enable Filter", filter.Enable); + AddWaitStep("Let track play", 10); + AddStep("Disable Filter", filter.Disable); + AddWaitStep("Let track play", 10); + AddStep("Stop Track", () => + { + testBeatmap.Track.Stop(); + }); + } + } +} From 4d91204faff435d6104a1333800f8b94e248a7d1 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 29 Sep 2021 20:29:27 +0900 Subject: [PATCH 075/391] Pause `Editor` sample playback while popup dialog is shown --- osu.Game/Screens/Edit/Editor.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 2ff0101dc0..1170658abb 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -508,6 +508,7 @@ namespace osu.Game.Screens.Edit if (isNewBeatmap || HasUnsavedChanges) { + samplePlaybackDisabled.Value = true; dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave, cancelExit)); return true; } @@ -756,7 +757,11 @@ namespace osu.Game.Screens.Edit ClipboardContent = editorBeatmap.BeatmapInfo.RulesetID == nextBeatmap.RulesetID ? clipboard.Value : string.Empty }); - private void cancelExit() => loader?.CancelPendingDifficultySwitch(); + private void cancelExit() + { + samplePlaybackDisabled.Value = false; + loader?.CancelPendingDifficultySwitch(); + } public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); From c83dd7d2b62117c09ab942fb5bd8aeaa790f624e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Sep 2021 20:52:03 +0900 Subject: [PATCH 076/391] Merge OnlinePlayComposite and RoomSubScreenComposite --- .../Match/BeatmapSelectionControl.cs | 2 +- .../Screens/OnlinePlay/OnlinePlayComposite.cs | 10 +++-- .../OnlinePlay/RoomSubScreenComposite.cs | 38 ------------------- 3 files changed, 8 insertions(+), 42 deletions(-) delete mode 100644 osu.Game/Screens/OnlinePlay/RoomSubScreenComposite.cs diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs index 6f1817a77c..35f30edf65 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs @@ -12,7 +12,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public class BeatmapSelectionControl : RoomSubScreenComposite + public class BeatmapSelectionControl : OnlinePlayComposite { [Resolved] private MultiplayerMatchSubScreen matchSubScreen { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index 722a2a0b94..aa971864ef 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -65,6 +65,9 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room))] protected Bindable Duration { get; private set; } + [Resolved(CanBeNull = true)] + private IBindable subScreenSelectedItem { get; set; } + /// /// The currently selected item in the , or the last item from /// if this is not within a . @@ -75,12 +78,13 @@ namespace osu.Game.Screens.OnlinePlay { base.LoadComplete(); + subScreenSelectedItem?.BindValueChanged(_ => UpdateSelectedItem()); Playlist.BindCollectionChanged((_, __) => UpdateSelectedItem(), true); } protected virtual void UpdateSelectedItem() - { - SelectedItem.Value = Playlist.LastOrDefault(); - } + => SelectedItem.Value = RoomID.Value == null || subScreenSelectedItem == null + ? Playlist.LastOrDefault() + : subScreenSelectedItem.Value; } } diff --git a/osu.Game/Screens/OnlinePlay/RoomSubScreenComposite.cs b/osu.Game/Screens/OnlinePlay/RoomSubScreenComposite.cs deleted file mode 100644 index 4cfd881aa3..0000000000 --- a/osu.Game/Screens/OnlinePlay/RoomSubScreenComposite.cs +++ /dev/null @@ -1,38 +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.Online.Rooms; -using osu.Game.Screens.OnlinePlay.Match; - -namespace osu.Game.Screens.OnlinePlay -{ - /// - /// An with additional logic tracking the currently-selected inside a . - /// - public class RoomSubScreenComposite : OnlinePlayComposite - { - [Resolved] - private IBindable subScreenSelectedItem { get; set; } - - protected override void LoadComplete() - { - base.LoadComplete(); - - subScreenSelectedItem.BindValueChanged(_ => UpdateSelectedItem(), true); - } - - protected override void UpdateSelectedItem() - { - if (RoomID.Value == null) - { - // If the room hasn't been created yet, fall-back to the base logic. - base.UpdateSelectedItem(); - return; - } - - SelectedItem.Value = subScreenSelectedItem.Value; - } - } -} From f55c4ac6406cabd1997f14e7643cf4a2156f8062 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 29 Sep 2021 21:05:46 +0900 Subject: [PATCH 077/391] IsLoaded check no longer required --- osu.Game/Overlays/DialogOverlay.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 3fade81cec..8a274968b7 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -89,8 +89,7 @@ namespace osu.Game.Overlays { base.PopOut(); - if (IsLoaded) - filter.Disable(); + filter.Disable(); if (CurrentDialog?.State.Value == Visibility.Visible) { From d14eed88fde0996855716be8394551c973a5d862 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Wed, 29 Sep 2021 19:14:54 +0000 Subject: [PATCH 078/391] final clean up before PR --- .../Difficulty/Skills/Speed.cs | 60 ++++++++----------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 5706e9f2cb..6b21ae7ac6 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -16,13 +16,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills public class Speed : OsuStrainSkill { private const double single_spacing_threshold = 125; - - private const double angle_bonus_begin = 5 * Math.PI / 6; - private const double pi_over_4 = Math.PI / 4; - private const double pi_over_2 = Math.PI / 2; - - private const double rhythm_multiplier = 2.0; + private const double rhythm_multiplier = 4.0; private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max. + private const double min_speed_bonus = 75; // ~200BPM + private const double max_speed_bonus = 45; + private const double speed_balancing_factor = 40; private double skillMultiplier => 1375; private double strainDecayBase => 0.3; @@ -33,11 +31,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills protected override int ReducedSectionCount => 5; protected override double DifficultyMultiplier => 1.04; - - private const double min_speed_bonus = 75; // ~200BPM - private const double max_speed_bonus = 45; - private const double speed_balancing_factor = 40; - protected override int HistoryLength => 32; private readonly double greatWindow; @@ -59,6 +52,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills int previousIslandSize = -1; double rhythmComplexitySum = 0; int islandSize = 0; + double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms bool firstDeltaSwitch = false; @@ -66,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { DifficultyHitObject currObj = Previous[i - 1]; DifficultyHitObject prevObj = Previous[i]; - DifficultyHitObject prevPrevObj = Previous[i + 1]; + DifficultyHitObject lastObj = Previous[i + 1]; double currHistoricalDecay = Math.Max(0, (history_time_max - (current.StartTime - currObj.StartTime))) / history_time_max; // scales note 0 to 1 from history to now @@ -76,11 +70,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills double currDelta = Math.Max(25, currObj.DeltaTime); double prevDelta = Math.Max(25, prevObj.DeltaTime); - double prevPrevDelta = ((OsuDifficultyHitObject)prevPrevObj).StrainTime; + double lastDelta = ((OsuDifficultyHitObject)lastObj).StrainTime; double effectiveRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta); if (effectiveRatio > 0.5) - effectiveRatio = 0.5 + (effectiveRatio - 0.5) * 5; // large buff for 1/3 -> 1/4 type transitions. + effectiveRatio = 0.5 + (effectiveRatio - 0.5) * 6; // large buff for 1/3 -> 1/4 type transitions. + else + effectiveRatio = 0.5; effectiveRatio *= currHistoricalDecay; // scale with time @@ -92,10 +88,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills } else { - - - if (islandSize > 6) - islandSize = 6; + if (islandSize > 12) + islandSize = 12; if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window effectiveRatio *= 0.25; @@ -104,12 +98,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills effectiveRatio *= 0.5; if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet) - effectiveRatio *= 0.25; + effectiveRatio *= 0.35; - if (prevPrevDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this. + if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5) + effectiveRatio *= 0.75; + + if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this. effectiveRatio *= 0.125; - rhythmComplexitySum += effectiveRatio; + rhythmComplexitySum += effectiveRatio * startRatio; + + startRatio = Math.Sqrt(Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta)); previousIslandSize = islandSize; // log the last island size. @@ -124,11 +123,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills // Begin counting island until we change speed again. firstDeltaSwitch = true; islandSize = 0; + startRatio = Math.Sqrt(Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta)); } } } - return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier * Math.Sqrt(52 / greatWindowFull)) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though) + if (greatWindowFull > 62) + rhythmComplexitySum *= Math.Sqrt(62 / greatWindowFull); + + return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though) } private double tapStrainOf(DifficultyHitObject current, double speedBonus, double strainTime) @@ -147,19 +150,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills var osuCurrObj = (OsuDifficultyHitObject)current; double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.JumpDistance); - double angleBonus = 1.0; - if (osuCurrObj.Angle != null) - { - double angle = osuCurrObj.Angle.Value; - - if (angle < pi_over_2) - angleBonus = 1.25; - else if (angle < angle_bonus_begin) - angleBonus = 1 + Math.Pow(Math.Sin(1.5 * (angle_bonus_begin - angle)), 2) / 4; - } - - return (angleBonus * speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime; + return (speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime; } private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); From 56b3c8aa9a72be245240de2eea652b8a77a5a64d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 11:52:14 +0900 Subject: [PATCH 079/391] Remove forgotten text --- .../Lounge/Components/DrawableRoomParticipantsList.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index 3d366a4c92..db1686ef4a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -80,8 +80,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Text = "hosted by smoogipoo" + AutoSizeAxes = Axes.Both } } }, From 619a907c47df793c6c03e1c6bb111423acff02b4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 12:01:26 +0900 Subject: [PATCH 080/391] Fix zero height grid --- osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 9e693b0b0c..f2af38c2df 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -333,6 +333,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { new Dimension(GridSizeMode.AutoSize), }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, Content = new[] { new Drawable[] From 18ab6747f73306610a607bec265bacffa009d16a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 12:01:28 +0900 Subject: [PATCH 081/391] Fix tests --- .../TestSceneDrawableRoomParticipantsList.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs index 6909bc3f97..982dfc5cd9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomParticipantsList.cs @@ -80,9 +80,9 @@ namespace osu.Game.Tests.Visual.Multiplayer for (int i = 0; i < 8; i++) { AddStep("remove user", () => removeUserAt(0)); - int remainingUsers = 7 - i; + int remainingUsers = 8 - i; - int displayedUsers = remainingUsers > 3 ? 2 : remainingUsers; + int displayedUsers = remainingUsers > 4 ? 3 : remainingUsers; AddAssert($"{displayedUsers} avatars displayed", () => list.ChildrenOfType().Count() == displayedUsers); } } @@ -97,11 +97,11 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddStep("set 3 circles", () => list.NumberOfCircles = 3); - AddAssert("2 users displayed", () => list.ChildrenOfType().Count() == 2); + AddAssert("3 users displayed", () => list.ChildrenOfType().Count() == 3); AddAssert("48 hidden users", () => list.ChildrenOfType().Single().Count == 48); AddStep("set 10 circles", () => list.NumberOfCircles = 10); - AddAssert("9 users displayed", () => list.ChildrenOfType().Count() == 9); + AddAssert("10 users displayed", () => list.ChildrenOfType().Count() == 10); AddAssert("41 hidden users", () => list.ChildrenOfType().Single().Count == 41); } @@ -115,24 +115,24 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddStep("remove from start", () => removeUserAt(0)); - AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); + AddAssert("4 circles displayed", () => list.ChildrenOfType().Count() == 4); AddAssert("46 hidden users", () => list.ChildrenOfType().Single().Count == 46); AddStep("remove from end", () => removeUserAt(SelectedRoom.Value.RecentParticipants.Count - 1)); - AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); + AddAssert("4 circles displayed", () => list.ChildrenOfType().Count() == 4); AddAssert("45 hidden users", () => list.ChildrenOfType().Single().Count == 45); AddRepeatStep("remove 45 users", () => removeUserAt(0), 45); - AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); + AddAssert("4 circles displayed", () => list.ChildrenOfType().Count() == 4); AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); AddAssert("hidden users bubble hidden", () => list.ChildrenOfType().Single().Alpha < 0.5f); AddStep("remove another user", () => removeUserAt(0)); - AddAssert("2 circles displayed", () => list.ChildrenOfType().Count() == 2); + AddAssert("3 circles displayed", () => list.ChildrenOfType().Count() == 3); AddAssert("0 hidden users", () => list.ChildrenOfType().Single().Count == 0); AddRepeatStep("remove the remaining two users", () => removeUserAt(0), 2); - AddAssert("0 circles displayed", () => !list.ChildrenOfType().Any()); + AddAssert("1 circle displayed", () => list.ChildrenOfType().Count() == 1); } private void addUser(int id) From ea30445efc41a508c4fe865956ccabee13b7d9b3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 12:03:34 +0900 Subject: [PATCH 082/391] Remove verbatim string --- .../Lounge/Components/DrawableRoomParticipantsList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index db1686ef4a..e9be805d44 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -261,7 +261,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components hostAvatar.User = host.NewValue; hostText.Clear(); - hostText.AddText(@"hosted by "); + hostText.AddText("hosted by "); if (host.NewValue != null) hostText.AddUserLink(host.NewValue); } From 202a602d2f8f249aa0bfc33ff79cc4f1e91d4226 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 12:03:44 +0900 Subject: [PATCH 083/391] Change default status to "ready to play" --- osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index f2af38c2df..03d13c353a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -372,7 +372,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components if (Type.Value == MatchType.Playlists) { - statusText.Text = "Waiting for players"; + statusText.Text = "Ready to play"; return; } From 816018edb71b81dcee304f0e850216cd4abd5486 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 12:04:30 +0900 Subject: [PATCH 084/391] Move hosted by text into nullcheck --- .../Lounge/Components/DrawableRoomParticipantsList.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs index e9be805d44..31eb5db9bc 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoomParticipantsList.cs @@ -261,9 +261,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components hostAvatar.User = host.NewValue; hostText.Clear(); - hostText.AddText("hosted by "); if (host.NewValue != null) + { + hostText.AddText("hosted by "); hostText.AddUserLink(host.NewValue); + } } private class CircularAvatar : CompositeDrawable From 6ffd9fdcfa20476f80a45fbbd60602782323148d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 14:46:01 +0900 Subject: [PATCH 085/391] Split out `BeatmapOnlineLookupQueue` from `BeatmapManager` --- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 4 +- osu.Game/Beatmaps/BeatmapManager.cs | 25 +- ...BeatmapManager_BeatmapOnlineLookupQueue.cs | 215 ------------------ osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs | 215 ++++++++++++++++++ osu.Game/OsuGameBase.cs | 9 +- 5 files changed, 234 insertions(+), 234 deletions(-) delete mode 100644 osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs create mode 100644 osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 7e7e5ebc45..a7d34fadbe 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -161,8 +161,8 @@ namespace osu.Game.Tests.Online protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => new TestDownloadRequest(set); - public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) - : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap, performOnlineLookups) + public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) + : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap) { } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index bd85017d58..a2f9740779 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -40,7 +40,7 @@ namespace osu.Game.Beatmaps /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// [ExcludeFromDynamicCompile] - public partial class BeatmapManager : DownloadableArchiveModelManager, IDisposable, IBeatmapResourceProvider + public partial class BeatmapManager : DownloadableArchiveModelManager, IBeatmapResourceProvider { /// /// Fired when a single difficulty has been hidden. @@ -54,6 +54,12 @@ namespace osu.Game.Beatmaps /// public IBindable> BeatmapRestored => beatmapRestored; + /// + /// A function which populates online information during the import process. + /// It is run as the final step of import. + /// + public Func PopulateOnlineInformation; + private readonly Bindable> beatmapRestored = new Bindable>(); /// @@ -79,11 +85,8 @@ namespace osu.Game.Beatmaps [CanBeNull] private readonly GameHost host; - [CanBeNull] - private readonly BeatmapOnlineLookupQueue onlineLookupQueue; - public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, - WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) + WorkingBeatmap defaultBeatmap = null) : base(storage, contextFactory, api, new BeatmapStore(contextFactory), host) { this.rulesets = rulesets; @@ -99,9 +102,6 @@ namespace osu.Game.Beatmaps beatmaps.ItemRemoved += removeWorkingCache; beatmaps.ItemUpdated += removeWorkingCache; - if (performOnlineLookups) - onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage); - largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)); trackStore = audioManager.GetTrackStore(Files.Store); } @@ -156,8 +156,8 @@ namespace osu.Game.Beatmaps bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); - if (onlineLookupQueue != null) - await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false); + if (PopulateOnlineInformation != null) + await PopulateOnlineInformation(beatmapSet, cancellationToken).ConfigureAwait(false); // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) @@ -533,11 +533,6 @@ namespace osu.Game.Beatmaps } } - public void Dispose() - { - onlineLookupQueue?.Dispose(); - } - #region IResourceStorageProvider TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs deleted file mode 100644 index 3dd34f6c2f..0000000000 --- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs +++ /dev/null @@ -1,215 +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.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Data.Sqlite; -using osu.Framework.Development; -using osu.Framework.IO.Network; -using osu.Framework.Logging; -using osu.Framework.Platform; -using osu.Framework.Testing; -using osu.Framework.Threading; -using osu.Game.Database; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; -using SharpCompress.Compressors; -using SharpCompress.Compressors.BZip2; - -namespace osu.Game.Beatmaps -{ - public partial class BeatmapManager - { - [ExcludeFromDynamicCompile] - private class BeatmapOnlineLookupQueue : IDisposable - { - private readonly IAPIProvider api; - private readonly Storage storage; - - private const int update_queue_request_concurrency = 4; - - private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(update_queue_request_concurrency, nameof(BeatmapOnlineLookupQueue)); - - private FileWebRequest cacheDownloadRequest; - - private const string cache_database_name = "online.db"; - - public BeatmapOnlineLookupQueue(IAPIProvider api, Storage storage) - { - this.api = api; - this.storage = storage; - - // avoid downloading / using cache for unit tests. - if (!DebugUtils.IsNUnitRunning && !storage.Exists(cache_database_name)) - prepareLocalCache(); - } - - public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken) - { - return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray()); - } - - // todo: expose this when we need to do individual difficulty lookups. - protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmap, CancellationToken cancellationToken) - => Task.Factory.StartNew(() => lookup(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); - - private void lookup(BeatmapSetInfo set, BeatmapInfo beatmap) - { - if (checkLocalCache(set, beatmap)) - return; - - if (api?.State.Value != APIState.Online) - return; - - var req = new GetBeatmapRequest(beatmap); - - req.Failure += fail; - - try - { - // intentionally blocking to limit web request concurrency - api.Perform(req); - - var res = req.Result; - - if (res != null) - { - beatmap.Status = res.Status; - beatmap.BeatmapSet.Status = res.BeatmapSet.Status; - beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; - beatmap.OnlineBeatmapID = res.OnlineBeatmapID; - - if (beatmap.Metadata != null) - beatmap.Metadata.AuthorID = res.AuthorID; - - if (beatmap.BeatmapSet.Metadata != null) - beatmap.BeatmapSet.Metadata.AuthorID = res.AuthorID; - - LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); - } - } - catch (Exception e) - { - fail(e); - } - - void fail(Exception e) - { - beatmap.OnlineBeatmapID = null; - LogForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); - } - } - - private void prepareLocalCache() - { - string cacheFilePath = storage.GetFullPath(cache_database_name); - string compressedCacheFilePath = $"{cacheFilePath}.bz2"; - - cacheDownloadRequest = new FileWebRequest(compressedCacheFilePath, $"https://assets.ppy.sh/client-resources/{cache_database_name}.bz2?{DateTimeOffset.UtcNow:yyyyMMdd}"); - - cacheDownloadRequest.Failed += ex => - { - File.Delete(compressedCacheFilePath); - File.Delete(cacheFilePath); - - Logger.Log($"{nameof(BeatmapOnlineLookupQueue)}'s online cache download failed: {ex}", LoggingTarget.Database); - }; - - cacheDownloadRequest.Finished += () => - { - try - { - 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); - - // set to null on completion to allow lookups to begin using the new source - cacheDownloadRequest = null; - } - catch (Exception ex) - { - Logger.Log($"{nameof(BeatmapOnlineLookupQueue)}'s online cache extraction failed: {ex}", LoggingTarget.Database); - File.Delete(cacheFilePath); - } - finally - { - File.Delete(compressedCacheFilePath); - } - }; - - cacheDownloadRequest.PerformAsync(); - } - - private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmap) - { - // download is in progress (or was, and failed). - if (cacheDownloadRequest != null) - return false; - - // database is unavailable. - if (!storage.Exists(cache_database_name)) - return false; - - if (string.IsNullOrEmpty(beatmap.MD5Hash) - && string.IsNullOrEmpty(beatmap.Path) - && beatmap.OnlineBeatmapID == null) - return false; - - try - { - using (var db = new SqliteConnection(DatabaseContextFactory.CreateDatabaseConnectionString("online.db", storage))) - { - db.Open(); - - using (var cmd = db.CreateCommand()) - { - cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved, user_id FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path"; - - cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmap.MD5Hash)); - cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmap.OnlineBeatmapID ?? (object)DBNull.Value)); - cmd.Parameters.Add(new SqliteParameter("@Path", beatmap.Path)); - - using (var reader = cmd.ExecuteReader()) - { - if (reader.Read()) - { - var status = (BeatmapSetOnlineStatus)reader.GetByte(2); - - beatmap.Status = status; - beatmap.BeatmapSet.Status = status; - beatmap.BeatmapSet.OnlineBeatmapSetID = reader.GetInt32(0); - beatmap.OnlineBeatmapID = reader.GetInt32(1); - - if (beatmap.Metadata != null) - beatmap.Metadata.AuthorID = reader.GetInt32(3); - - if (beatmap.BeatmapSet.Metadata != null) - beatmap.BeatmapSet.Metadata.AuthorID = reader.GetInt32(3); - - LogForModel(set, $"Cached local retrieval for {beatmap}."); - return true; - } - } - } - } - } - catch (Exception ex) - { - LogForModel(set, $"Cached local retrieval for {beatmap} failed with {ex}."); - } - - return false; - } - - public void Dispose() - { - cacheDownloadRequest?.Dispose(); - updateScheduler?.Dispose(); - } - } - } -} diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs new file mode 100644 index 0000000000..bbac30f2bb --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -0,0 +1,215 @@ +// 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 System.Threading.Tasks; +using Microsoft.Data.Sqlite; +using osu.Framework.Development; +using osu.Framework.IO.Network; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Framework.Threading; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using SharpCompress.Compressors; +using SharpCompress.Compressors.BZip2; + +namespace osu.Game.Beatmaps +{ + [ExcludeFromDynamicCompile] + public class BeatmapOnlineLookupQueue : IDisposable + { + private readonly IAPIProvider api; + private readonly Storage storage; + + private const int update_queue_request_concurrency = 4; + + private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(update_queue_request_concurrency, nameof(BeatmapOnlineLookupQueue)); + + private FileWebRequest cacheDownloadRequest; + + private const string cache_database_name = "online.db"; + + public BeatmapOnlineLookupQueue(IAPIProvider api, Storage storage) + { + this.api = api; + this.storage = storage; + + // avoid downloading / using cache for unit tests. + if (!DebugUtils.IsNUnitRunning && !storage.Exists(cache_database_name)) + prepareLocalCache(); + } + + public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken) + { + return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray()); + } + + // todo: expose this when we need to do individual difficulty lookups. + protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmap, CancellationToken cancellationToken) + => Task.Factory.StartNew(() => lookup(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + + private void lookup(BeatmapSetInfo set, BeatmapInfo beatmap) + { + if (checkLocalCache(set, beatmap)) + return; + + if (api?.State.Value != APIState.Online) + return; + + var req = new GetBeatmapRequest(beatmap); + + req.Failure += fail; + + try + { + // intentionally blocking to limit web request concurrency + api.Perform(req); + + var res = req.Result; + + if (res != null) + { + beatmap.Status = res.Status; + beatmap.BeatmapSet.Status = res.BeatmapSet.Status; + beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; + beatmap.OnlineBeatmapID = res.OnlineBeatmapID; + + if (beatmap.Metadata != null) + beatmap.Metadata.AuthorID = res.AuthorID; + + if (beatmap.BeatmapSet.Metadata != null) + beatmap.BeatmapSet.Metadata.AuthorID = res.AuthorID; + + logForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); + } + } + catch (Exception e) + { + fail(e); + } + + void fail(Exception e) + { + beatmap.OnlineBeatmapID = null; + logForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); + } + } + + private void prepareLocalCache() + { + string cacheFilePath = storage.GetFullPath(cache_database_name); + string compressedCacheFilePath = $"{cacheFilePath}.bz2"; + + cacheDownloadRequest = new FileWebRequest(compressedCacheFilePath, $"https://assets.ppy.sh/client-resources/{cache_database_name}.bz2?{DateTimeOffset.UtcNow:yyyyMMdd}"); + + cacheDownloadRequest.Failed += ex => + { + File.Delete(compressedCacheFilePath); + File.Delete(cacheFilePath); + + Logger.Log($"{nameof(BeatmapOnlineLookupQueue)}'s online cache download failed: {ex}", LoggingTarget.Database); + }; + + cacheDownloadRequest.Finished += () => + { + try + { + 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); + + // set to null on completion to allow lookups to begin using the new source + cacheDownloadRequest = null; + } + catch (Exception ex) + { + Logger.Log($"{nameof(BeatmapOnlineLookupQueue)}'s online cache extraction failed: {ex}", LoggingTarget.Database); + File.Delete(cacheFilePath); + } + finally + { + File.Delete(compressedCacheFilePath); + } + }; + + cacheDownloadRequest.PerformAsync(); + } + + private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmap) + { + // download is in progress (or was, and failed). + if (cacheDownloadRequest != null) + return false; + + // database is unavailable. + if (!storage.Exists(cache_database_name)) + return false; + + if (string.IsNullOrEmpty(beatmap.MD5Hash) + && string.IsNullOrEmpty(beatmap.Path) + && beatmap.OnlineBeatmapID == null) + return false; + + try + { + using (var db = new SqliteConnection(DatabaseContextFactory.CreateDatabaseConnectionString("online.db", storage))) + { + db.Open(); + + using (var cmd = db.CreateCommand()) + { + cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved, user_id FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path"; + + cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmap.MD5Hash)); + cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmap.OnlineBeatmapID ?? (object)DBNull.Value)); + cmd.Parameters.Add(new SqliteParameter("@Path", beatmap.Path)); + + using (var reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + var status = (BeatmapSetOnlineStatus)reader.GetByte(2); + + beatmap.Status = status; + beatmap.BeatmapSet.Status = status; + beatmap.BeatmapSet.OnlineBeatmapSetID = reader.GetInt32(0); + beatmap.OnlineBeatmapID = reader.GetInt32(1); + + if (beatmap.Metadata != null) + beatmap.Metadata.AuthorID = reader.GetInt32(3); + + if (beatmap.BeatmapSet.Metadata != null) + beatmap.BeatmapSet.Metadata.AuthorID = reader.GetInt32(3); + + logForModel(set, $"Cached local retrieval for {beatmap}."); + return true; + } + } + } + } + } + catch (Exception ex) + { + logForModel(set, $"Cached local retrieval for {beatmap} failed with {ex}."); + } + + return false; + } + + private void logForModel(BeatmapSetInfo set, string message) => + ArchiveModelManager.LogForModel(set, $"{nameof(BeatmapOnlineLookupQueue)}] {message}"); + + public void Dispose() + { + cacheDownloadRequest?.Dispose(); + updateScheduler?.Dispose(); + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 7aa460981a..8263e26dec 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -138,6 +138,8 @@ namespace osu.Game private UserLookupCache userCache; + private BeatmapOnlineLookupQueue onlineBeatmapLookupCache; + private FileStore fileStore; private RulesetConfigCache rulesetConfigCache; @@ -242,7 +244,11 @@ namespace osu.Game // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Scheduler, Host, () => difficultyCache, LocalConfig)); - dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, true)); + dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap)); + + onlineBeatmapLookupCache = new BeatmapOnlineLookupQueue(API, Storage); + + BeatmapManager.PopulateOnlineInformation = onlineBeatmapLookupCache.UpdateAsync; // this should likely be moved to ArchiveModelManager when another case appears where it is necessary // to have inter-dependent model managers. this could be obtained with an IHasForeign interface to @@ -524,7 +530,6 @@ namespace osu.Game base.Dispose(isDisposing); RulesetStore?.Dispose(); - BeatmapManager?.Dispose(); LocalConfig?.Dispose(); contextFactory?.FlushConnections(); From 8a6501fa58f67f49aa71df20dbb76a5caba92516 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 14:46:07 +0900 Subject: [PATCH 086/391] Add basic component level xmldoc --- osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index bbac30f2bb..19f02c82ec 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -21,6 +21,13 @@ using SharpCompress.Compressors.BZip2; namespace osu.Game.Beatmaps { + /// + /// A component which handles population of online IDs for beatmaps using a two part lookup procedure. + /// + /// + /// On creating the component, a copy of a database containing metadata for a large subset of beatmaps (stored to ). + /// This will always be checked before doing a second online query to get required metadata. + /// [ExcludeFromDynamicCompile] public class BeatmapOnlineLookupQueue : IDisposable { From 968826c9e7b0b65015c9d26c250695ab2887c973 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 30 Sep 2021 15:17:39 +0900 Subject: [PATCH 087/391] Fix typo --- osu.Game/Audio/Effects/Filter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Audio/Effects/Filter.cs b/osu.Game/Audio/Effects/Filter.cs index 37a2fa93db..04ad862879 100644 --- a/osu.Game/Audio/Effects/Filter.cs +++ b/osu.Game/Audio/Effects/Filter.cs @@ -39,7 +39,7 @@ namespace osu.Game.Audio.Effects public void Disable() { - this.TransformBindableTo(filterFreq, SweepCutoffStart, SweepDuration, SweepEasing).OnComplete(_ => detatchFilter()); + this.TransformBindableTo(filterFreq, SweepCutoffStart, SweepDuration, SweepEasing).OnComplete(_ => detachFilter()); } private void attachFilter() @@ -57,7 +57,7 @@ namespace osu.Game.Audio.Effects IsActive = true; } - private void detatchFilter() + private void detachFilter() { if (!IsActive) return; @@ -81,7 +81,7 @@ namespace osu.Game.Audio.Effects protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - detatchFilter(); + detachFilter(); } } } From 1304b55c4182db06c22b5830cb449584c3e18dde Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 30 Sep 2021 15:19:09 +0900 Subject: [PATCH 088/391] Move visual test to correct namespace --- osu.Game.Tests/{ => Visual}/Audio/TestSceneFilter.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename osu.Game.Tests/{ => Visual}/Audio/TestSceneFilter.cs (97%) diff --git a/osu.Game.Tests/Audio/TestSceneFilter.cs b/osu.Game.Tests/Visual/Audio/TestSceneFilter.cs similarity index 97% rename from osu.Game.Tests/Audio/TestSceneFilter.cs rename to osu.Game.Tests/Visual/Audio/TestSceneFilter.cs index fd4fe530eb..6a7a404d8a 100644 --- a/osu.Game.Tests/Audio/TestSceneFilter.cs +++ b/osu.Game.Tests/Visual/Audio/TestSceneFilter.cs @@ -8,9 +8,8 @@ using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Game.Audio.Effects; using osu.Game.Beatmaps; -using osu.Game.Tests.Visual; -namespace osu.Game.Tests.Audio +namespace osu.Game.Tests.Visual.Audio { public class TestSceneFilter : OsuTestScene { From e7e04733234cf2de40abf9241548c786f64d8ed6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 15:40:41 +0900 Subject: [PATCH 089/391] Split out `WorkingBeatmapCache` from `BeatmapManager` --- osu.Game/Beatmaps/BeatmapManager.cs | 134 +-------- .../Beatmaps/BeatmapManager_WorkingBeatmap.cs | 147 --------- osu.Game/Beatmaps/WorkingBeatmapCache.cs | 279 ++++++++++++++++++ 3 files changed, 289 insertions(+), 271 deletions(-) delete mode 100644 osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs create mode 100644 osu.Game/Beatmaps/WorkingBeatmapCache.cs diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index a2f9740779..1c0e7dc319 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -9,18 +9,13 @@ using System.Linq.Expressions; using System.Text; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; -using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics.Textures; -using osu.Framework.IO.Stores; -using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Platform; -using osu.Framework.Statistics; using osu.Framework.Testing; using osu.Game.Beatmaps.Formats; using osu.Game.Database; @@ -31,7 +26,6 @@ using osu.Game.Online.API.Requests; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Skinning; -using osu.Game.Users; using Decoder = osu.Game.Beatmaps.Formats.Decoder; namespace osu.Game.Beatmaps @@ -40,7 +34,7 @@ namespace osu.Game.Beatmaps /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// [ExcludeFromDynamicCompile] - public partial class BeatmapManager : DownloadableArchiveModelManager, IBeatmapResourceProvider + public class BeatmapManager : DownloadableArchiveModelManager { /// /// Fired when a single difficulty has been hidden. @@ -60,12 +54,12 @@ namespace osu.Game.Beatmaps /// public Func PopulateOnlineInformation; - private readonly Bindable> beatmapRestored = new Bindable>(); - /// - /// A default representation of a WorkingBeatmap to use when no beatmap is available. + /// The game working beatmap cache, used to invalidate entries on changes. /// - public readonly WorkingBeatmap DefaultBeatmap; + public WorkingBeatmapCache WorkingBeatmapCache { private get; set; } + + private readonly Bindable> beatmapRestored = new Bindable>(); public override IEnumerable HandledExtensions => new[] { ".osz" }; @@ -75,35 +69,19 @@ namespace osu.Game.Beatmaps protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage(); - private readonly RulesetStore rulesets; private readonly BeatmapStore beatmaps; - private readonly AudioManager audioManager; - private readonly IResourceStore resources; - private readonly LargeTextureStore largeTextureStore; - private readonly ITrackStore trackStore; + private readonly RulesetStore rulesets; - [CanBeNull] - private readonly GameHost host; - - public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, - WorkingBeatmap defaultBeatmap = null) + public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host = null) : base(storage, contextFactory, api, new BeatmapStore(contextFactory), host) { this.rulesets = rulesets; - this.audioManager = audioManager; - this.resources = resources; - this.host = host; - - DefaultBeatmap = defaultBeatmap; beatmaps = (BeatmapStore)ModelStore; beatmaps.BeatmapHidden += b => beatmapHidden.Value = new WeakReference(b); beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference(b); - beatmaps.ItemRemoved += removeWorkingCache; - beatmaps.ItemUpdated += removeWorkingCache; - - largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)); - trackStore = audioManager.GetTrackStore(Files.Store); + beatmaps.ItemRemoved += b => WorkingBeatmapCache?.Invalidate(b); + beatmaps.ItemUpdated += obj => WorkingBeatmapCache?.Invalidate(obj); } protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => @@ -111,33 +89,6 @@ namespace osu.Game.Beatmaps protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; - public WorkingBeatmap CreateNew(RulesetInfo ruleset, User user) - { - var metadata = new BeatmapMetadata - { - Author = user, - }; - - var set = new BeatmapSetInfo - { - Metadata = metadata, - Beatmaps = new List - { - new BeatmapInfo - { - BaseDifficulty = new BeatmapDifficulty(), - Ruleset = ruleset, - Metadata = metadata, - WidescreenStoryboard = true, - SamplesMatchPlaybackRate = true, - } - } - }; - - var working = Import(set).Result; - return GetWorkingBeatmap(working.Beatmaps.First()); - } - protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) { if (archive != null) @@ -278,43 +229,7 @@ namespace osu.Game.Beatmaps } } - removeWorkingCache(info); - } - - private readonly WeakList workingCache = new WeakList(); - - /// - /// Retrieve a instance for the provided - /// - /// The beatmap to lookup. - /// A instance correlating to the provided . - public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) - { - // if there are no files, presume the full beatmap info has not yet been fetched from the database. - if (beatmapInfo?.BeatmapSet?.Files.Count == 0) - { - int lookupId = beatmapInfo.ID; - beatmapInfo = QueryBeatmap(b => b.ID == lookupId); - } - - if (beatmapInfo?.BeatmapSet == null) - return DefaultBeatmap; - - lock (workingCache) - { - var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); - if (working != null) - return working; - - beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata; - - workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this)); - - // best effort; may be higher than expected. - GlobalStatistics.Get(nameof(Beatmaps), $"Cached {nameof(WorkingBeatmap)}s").Value = workingCache.Count(); - - return working; - } + WorkingBeatmapCache?.Invalidate(info); } /// @@ -515,35 +430,6 @@ namespace osu.Game.Beatmaps return endTime - startTime; } - private void removeWorkingCache(BeatmapSetInfo info) - { - if (info.Beatmaps == null) return; - - foreach (var b in info.Beatmaps) - removeWorkingCache(b); - } - - private void removeWorkingCache(BeatmapInfo info) - { - lock (workingCache) - { - var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == info.ID); - if (working != null) - workingCache.Remove(working); - } - } - - #region IResourceStorageProvider - - TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; - ITrackStore IBeatmapResourceProvider.Tracks => trackStore; - AudioManager IStorageResourceProvider.AudioManager => audioManager; - IResourceStore IStorageResourceProvider.Files => Files.Store; - IResourceStore IStorageResourceProvider.Resources => resources; - IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore); - - #endregion - /// /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. /// diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs deleted file mode 100644 index 45112ae74c..0000000000 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ /dev/null @@ -1,147 +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.IO; -using osu.Framework.Audio.Track; -using osu.Framework.Graphics.Textures; -using osu.Framework.Logging; -using osu.Framework.Testing; -using osu.Game.Beatmaps.Formats; -using osu.Game.IO; -using osu.Game.Skinning; -using osu.Game.Storyboards; - -namespace osu.Game.Beatmaps -{ - public partial class BeatmapManager - { - [ExcludeFromDynamicCompile] - private class BeatmapManagerWorkingBeatmap : WorkingBeatmap - { - [NotNull] - private readonly IBeatmapResourceProvider resources; - - public BeatmapManagerWorkingBeatmap(BeatmapInfo beatmapInfo, [NotNull] IBeatmapResourceProvider resources) - : base(beatmapInfo, resources.AudioManager) - { - this.resources = resources; - } - - protected override IBeatmap GetBeatmap() - { - if (BeatmapInfo.Path == null) - return new Beatmap { BeatmapInfo = BeatmapInfo }; - - try - { - using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path)))) - return Decoder.GetDecoder(stream).Decode(stream); - } - catch (Exception e) - { - Logger.Error(e, "Beatmap failed to load"); - return null; - } - } - - protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes. - - protected override Texture GetBackground() - { - if (Metadata?.BackgroundFile == null) - return null; - - try - { - return resources.LargeTextureStore.Get(BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile)); - } - catch (Exception e) - { - Logger.Error(e, "Background failed to load"); - return null; - } - } - - protected override Track GetBeatmapTrack() - { - if (Metadata?.AudioFile == null) - return null; - - try - { - return resources.Tracks.Get(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile)); - } - catch (Exception e) - { - Logger.Error(e, "Track failed to load"); - return null; - } - } - - protected override Waveform GetWaveform() - { - if (Metadata?.AudioFile == null) - return null; - - try - { - var trackData = GetStream(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile)); - return trackData == null ? null : new Waveform(trackData); - } - catch (Exception e) - { - Logger.Error(e, "Waveform failed to load"); - return null; - } - } - - protected override Storyboard GetStoryboard() - { - Storyboard storyboard; - - try - { - using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path)))) - { - var decoder = Decoder.GetDecoder(stream); - - // todo: support loading from both set-wide storyboard *and* beatmap specific. - if (BeatmapSetInfo?.StoryboardFile == null) - storyboard = decoder.Decode(stream); - else - { - using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapSetInfo.StoryboardFile)))) - storyboard = decoder.Decode(stream, secondaryStream); - } - } - } - catch (Exception e) - { - Logger.Error(e, "Storyboard failed to load"); - storyboard = new Storyboard(); - } - - storyboard.BeatmapInfo = BeatmapInfo; - - return storyboard; - } - - protected internal override ISkin GetSkin() - { - try - { - return new LegacyBeatmapSkin(BeatmapInfo, resources.Files, resources); - } - catch (Exception e) - { - Logger.Error(e, "Skin failed to load"); - return null; - } - } - - public override Stream GetStream(string storagePath) => resources.Files.GetStream(storagePath); - } - } -} diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs new file mode 100644 index 0000000000..9f40eb4898 --- /dev/null +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -0,0 +1,279 @@ +// 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 JetBrains.Annotations; +using osu.Framework.Audio; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using osu.Framework.Lists; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Statistics; +using osu.Framework.Testing; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO; +using osu.Game.Rulesets; +using osu.Game.Skinning; +using osu.Game.Storyboards; +using osu.Game.Users; + +namespace osu.Game.Beatmaps +{ + public class WorkingBeatmapCache : IBeatmapResourceProvider + { + private readonly WeakList workingCache = new WeakList(); + + /// + /// A default representation of a WorkingBeatmap to use when no beatmap is available. + /// + public readonly WorkingBeatmap DefaultBeatmap; + + public BeatmapManager BeatmapManager { private get; set; } + + private readonly AudioManager audioManager; + private readonly IResourceStore resources; + private readonly LargeTextureStore largeTextureStore; + private readonly ITrackStore trackStore; + private readonly IResourceStore files; + + [CanBeNull] + private readonly GameHost host; + + public WorkingBeatmapCache([NotNull] AudioManager audioManager, IResourceStore resources, IResourceStore files, WorkingBeatmap defaultBeatmap = null, GameHost host = null) + { + DefaultBeatmap = defaultBeatmap; + + this.audioManager = audioManager; + this.resources = resources; + this.host = host; + this.files = files; + largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(files)); + trackStore = audioManager.GetTrackStore(files); + } + + public void Invalidate(BeatmapSetInfo info) + { + if (info.Beatmaps == null) return; + + foreach (var b in info.Beatmaps) + Invalidate(b); + } + + public void Invalidate(BeatmapInfo info) + { + lock (workingCache) + { + var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == info.ID); + if (working != null) + workingCache.Remove(working); + } + } + + /// + /// Create a new . + /// + public WorkingBeatmap CreateNew(RulesetInfo ruleset, User user) + { + var metadata = new BeatmapMetadata + { + Author = user, + }; + + var set = new BeatmapSetInfo + { + Metadata = metadata, + Beatmaps = new List + { + new BeatmapInfo + { + BaseDifficulty = new BeatmapDifficulty(), + Ruleset = ruleset, + Metadata = metadata, + WidescreenStoryboard = true, + SamplesMatchPlaybackRate = true, + } + } + }; + + var working = BeatmapManager.Import(set).Result; + return GetWorkingBeatmap(working.Beatmaps.First()); + } + + /// + /// Retrieve a instance for the provided + /// + /// The beatmap to lookup. + /// A instance correlating to the provided . + public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) + { + // if there are no files, presume the full beatmap info has not yet been fetched from the database. + if (beatmapInfo?.BeatmapSet?.Files.Count == 0) + { + int lookupId = beatmapInfo.ID; + beatmapInfo = BeatmapManager.QueryBeatmap(b => b.ID == lookupId); + } + + if (beatmapInfo?.BeatmapSet == null) + return DefaultBeatmap; + + lock (workingCache) + { + var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); + if (working != null) + return working; + + beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata; + + workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this)); + + // best effort; may be higher than expected. + GlobalStatistics.Get(nameof(Beatmaps), $"Cached {nameof(WorkingBeatmap)}s").Value = workingCache.Count(); + + return working; + } + } + + #region IResourceStorageProvider + + TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; + ITrackStore IBeatmapResourceProvider.Tracks => trackStore; + AudioManager IStorageResourceProvider.AudioManager => audioManager; + IResourceStore IStorageResourceProvider.Files => files; + IResourceStore IStorageResourceProvider.Resources => resources; + IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore); + + #endregion + + [ExcludeFromDynamicCompile] + private class BeatmapManagerWorkingBeatmap : WorkingBeatmap + { + [NotNull] + private readonly IBeatmapResourceProvider resources; + + public BeatmapManagerWorkingBeatmap(BeatmapInfo beatmapInfo, [NotNull] IBeatmapResourceProvider resources) + : base(beatmapInfo, resources.AudioManager) + { + this.resources = resources; + } + + protected override IBeatmap GetBeatmap() + { + if (BeatmapInfo.Path == null) + return new Beatmap { BeatmapInfo = BeatmapInfo }; + + try + { + using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path)))) + return Decoder.GetDecoder(stream).Decode(stream); + } + catch (Exception e) + { + Logger.Error(e, "Beatmap failed to load"); + return null; + } + } + + protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes. + + protected override Texture GetBackground() + { + if (Metadata?.BackgroundFile == null) + return null; + + try + { + return resources.LargeTextureStore.Get(BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile)); + } + catch (Exception e) + { + Logger.Error(e, "Background failed to load"); + return null; + } + } + + protected override Track GetBeatmapTrack() + { + if (Metadata?.AudioFile == null) + return null; + + try + { + return resources.Tracks.Get(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile)); + } + catch (Exception e) + { + Logger.Error(e, "Track failed to load"); + return null; + } + } + + protected override Waveform GetWaveform() + { + if (Metadata?.AudioFile == null) + return null; + + try + { + var trackData = GetStream(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile)); + return trackData == null ? null : new Waveform(trackData); + } + catch (Exception e) + { + Logger.Error(e, "Waveform failed to load"); + return null; + } + } + + protected override Storyboard GetStoryboard() + { + Storyboard storyboard; + + try + { + using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path)))) + { + var decoder = Decoder.GetDecoder(stream); + + // todo: support loading from both set-wide storyboard *and* beatmap specific. + if (BeatmapSetInfo?.StoryboardFile == null) + storyboard = decoder.Decode(stream); + else + { + using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapSetInfo.StoryboardFile)))) + storyboard = decoder.Decode(stream, secondaryStream); + } + } + } + catch (Exception e) + { + Logger.Error(e, "Storyboard failed to load"); + storyboard = new Storyboard(); + } + + storyboard.BeatmapInfo = BeatmapInfo; + + return storyboard; + } + + protected internal override ISkin GetSkin() + { + try + { + return new LegacyBeatmapSkin(BeatmapInfo, resources.Files, resources); + } + catch (Exception e) + { + Logger.Error(e, "Skin failed to load"); + return null; + } + } + + public override Stream GetStream(string storagePath) => resources.Files.GetStream(storagePath); + } + } +} From d21139b03efb77e7f5aeeea2d8236320d0e0d693 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 15:43:49 +0900 Subject: [PATCH 090/391] Split out database portion from `BeatmapManager` --- osu.Game/Beatmaps/BeatmapManager.cs | 466 +--------------------- osu.Game/Beatmaps/BeatmapModelManager.cs | 479 +++++++++++++++++++++++ 2 files changed, 483 insertions(+), 462 deletions(-) create mode 100644 osu.Game/Beatmaps/BeatmapModelManager.cs diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 1c0e7dc319..c445925a90 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -1,479 +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.IO; -using System.Linq; -using System.Linq.Expressions; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using osu.Framework.Audio.Track; -using osu.Framework.Bindables; -using osu.Framework.Extensions; -using osu.Framework.Graphics.Textures; -using osu.Framework.Logging; -using osu.Framework.Platform; using osu.Framework.Testing; -using osu.Game.Beatmaps.Formats; -using osu.Game.Database; -using osu.Game.IO; -using osu.Game.IO.Archives; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Objects; -using osu.Game.Skinning; -using Decoder = osu.Game.Beatmaps.Formats.Decoder; namespace osu.Game.Beatmaps { /// - /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. + /// Handles general operations related to global beatmap management. /// [ExcludeFromDynamicCompile] - public class BeatmapManager : DownloadableArchiveModelManager + public class BeatmapManager { - /// - /// Fired when a single difficulty has been hidden. - /// - public IBindable> BeatmapHidden => beatmapHidden; - - private readonly Bindable> beatmapHidden = new Bindable>(); - - /// - /// Fired when a single difficulty has been restored. - /// - public IBindable> BeatmapRestored => beatmapRestored; - - /// - /// A function which populates online information during the import process. - /// It is run as the final step of import. - /// - public Func PopulateOnlineInformation; - - /// - /// The game working beatmap cache, used to invalidate entries on changes. - /// - public WorkingBeatmapCache WorkingBeatmapCache { private get; set; } - - private readonly Bindable> beatmapRestored = new Bindable>(); - - public override IEnumerable HandledExtensions => new[] { ".osz" }; - - protected override string[] HashableFileTypes => new[] { ".osu" }; - - protected override string ImportFromStablePath => "."; - - protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage(); - - private readonly BeatmapStore beatmaps; - private readonly RulesetStore rulesets; - - public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host = null) - : base(storage, contextFactory, api, new BeatmapStore(contextFactory), host) + public BeatmapManager() { - this.rulesets = rulesets; - - beatmaps = (BeatmapStore)ModelStore; - beatmaps.BeatmapHidden += b => beatmapHidden.Value = new WeakReference(b); - beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference(b); - beatmaps.ItemRemoved += b => WorkingBeatmapCache?.Invalidate(b); - beatmaps.ItemUpdated += obj => WorkingBeatmapCache?.Invalidate(obj); + beatmapModelManager = new BeatmapModelManager() } - protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => - new DownloadBeatmapSetRequest(set, minimiseDownloadSize); - - protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; - - protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) - { - if (archive != null) - beatmapSet.Beatmaps = createBeatmapDifficulties(beatmapSet.Files); - - foreach (BeatmapInfo b in beatmapSet.Beatmaps) - { - // remove metadata from difficulties where it matches the set - if (beatmapSet.Metadata.Equals(b.Metadata)) - b.Metadata = null; - - b.BeatmapSet = beatmapSet; - } - - validateOnlineIds(beatmapSet); - - bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); - - if (PopulateOnlineInformation != null) - await PopulateOnlineInformation(beatmapSet, cancellationToken).ConfigureAwait(false); - - // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. - if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) - { - if (beatmapSet.OnlineBeatmapSetID != null) - { - beatmapSet.OnlineBeatmapSetID = null; - LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs"); - } - } - } - - protected override void PreImport(BeatmapSetInfo beatmapSet) - { - if (beatmapSet.Beatmaps.Any(b => b.BaseDifficulty == null)) - throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}."); - - // check if a set already exists with the same online id, delete if it does. - if (beatmapSet.OnlineBeatmapSetID != null) - { - var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID); - - if (existingOnlineId != null) - { - Delete(existingOnlineId); - - // in order to avoid a unique key constraint, immediately remove the online ID from the previous set. - existingOnlineId.OnlineBeatmapSetID = null; - foreach (var b in existingOnlineId.Beatmaps) - b.OnlineBeatmapID = null; - - LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been deleted."); - } - } - } - - private void validateOnlineIds(BeatmapSetInfo beatmapSet) - { - var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList(); - - // ensure all IDs are unique - if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1)) - { - LogForModel(beatmapSet, "Found non-unique IDs, resetting..."); - resetIds(); - return; - } - - // find any existing beatmaps in the database that have matching online ids - var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).ToList(); - - if (existingBeatmaps.Count > 0) - { - // reset the import ids (to force a re-fetch) *unless* they match the candidate CheckForExisting set. - // we can ignore the case where the new ids are contained by the CheckForExisting set as it will either be used (import skipped) or deleted. - var existing = CheckForExisting(beatmapSet); - - if (existing == null || existingBeatmaps.Any(b => !existing.Beatmaps.Contains(b))) - { - LogForModel(beatmapSet, "Found existing import with IDs already, resetting..."); - resetIds(); - } - } - - void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null); - } - - protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable items) - => base.CheckLocalAvailability(model, items) - || (model.OnlineBeatmapSetID != null && items.Any(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID)); - - /// - /// Delete a beatmap difficulty. - /// - /// The beatmap difficulty to hide. - public void Hide(BeatmapInfo beatmap) => beatmaps.Hide(beatmap); - - /// - /// Restore a beatmap difficulty. - /// - /// The beatmap difficulty to restore. - public void Restore(BeatmapInfo beatmap) => beatmaps.Restore(beatmap); - - /// - /// Saves an file against a given . - /// - /// The to save the content against. The file referenced by will be replaced. - /// The content to write. - /// The beatmap content to write, null if to be omitted. - public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) - { - var setInfo = info.BeatmapSet; - - using (var stream = new MemoryStream()) - { - using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); - - stream.Seek(0, SeekOrigin.Begin); - - using (ContextFactory.GetForWrite()) - { - var beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == info.ID); - var metadata = beatmapInfo.Metadata ?? setInfo.Metadata; - - // grab the original file (or create a new one if not found). - var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo(); - - // metadata may have changed; update the path with the standard format. - beatmapInfo.Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.Version}].osu"; - beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); - - // update existing or populate new file's filename. - fileInfo.Filename = beatmapInfo.Path; - - stream.Seek(0, SeekOrigin.Begin); - ReplaceFile(setInfo, fileInfo, stream); - } - } - - WorkingBeatmapCache?.Invalidate(info); - } - - /// - /// Perform a lookup query on available s. - /// - /// The query. - /// The first result for the provided query, or null if no results were found. - public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query); - - protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import) - { - if (!base.CanSkipImport(existing, import)) - return false; - - return existing.Beatmaps.Any(b => b.OnlineBeatmapID != null); - } - - protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import) - { - if (!base.CanReuseExisting(existing, import)) - return false; - - var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); - var importIds = import.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); - - // force re-import if we are not in a sane state. - return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID && existingIds.SequenceEqual(importIds); - } - - /// - /// Returns a list of all usable s. - /// - /// A list of available . - public List GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) => - GetAllUsableBeatmapSetsEnumerable(includes, includeProtected).ToList(); - - /// - /// Returns a list of all usable s. Note that files are not populated. - /// - /// The level of detail to include in the returned objects. - /// Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases. - /// A list of available . - public IEnumerable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false) - { - IQueryable queryable; - - switch (includes) - { - case IncludedDetails.Minimal: - queryable = beatmaps.BeatmapSetsOverview; - break; - - case IncludedDetails.AllButRuleset: - queryable = beatmaps.BeatmapSetsWithoutRuleset; - break; - - case IncludedDetails.AllButFiles: - queryable = beatmaps.BeatmapSetsWithoutFiles; - break; - - default: - queryable = beatmaps.ConsumableItems; - break; - } - - // AsEnumerable used here to avoid applying the WHERE in sql. When done so, ef core 2.x uses an incorrect ORDER BY - // clause which causes queries to take 5-10x longer. - // TODO: remove if upgrading to EF core 3.x. - return queryable.AsEnumerable().Where(s => !s.DeletePending && (includeProtected || !s.Protected)); - } - - /// - /// Perform a lookup query on available s. - /// - /// The query. - /// The level of detail to include in the returned objects. - /// Results from the provided query. - public IEnumerable QueryBeatmapSets(Expression> query, IncludedDetails includes = IncludedDetails.All) - { - IQueryable queryable; - - switch (includes) - { - case IncludedDetails.Minimal: - queryable = beatmaps.BeatmapSetsOverview; - break; - - case IncludedDetails.AllButRuleset: - queryable = beatmaps.BeatmapSetsWithoutRuleset; - break; - - case IncludedDetails.AllButFiles: - queryable = beatmaps.BeatmapSetsWithoutFiles; - break; - - default: - queryable = beatmaps.ConsumableItems; - break; - } - - return queryable.AsNoTracking().Where(query); - } - - /// - /// Perform a lookup query on available s. - /// - /// The query. - /// The first result for the provided query, or null if no results were found. - public BeatmapInfo QueryBeatmap(Expression> query) => beatmaps.Beatmaps.AsNoTracking().FirstOrDefault(query); - - /// - /// Perform a lookup query on available s. - /// - /// The query. - /// Results from the provided query. - public IQueryable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); - - protected override string HumanisedModelName => "beatmap"; - - protected override BeatmapSetInfo CreateModel(ArchiveReader reader) - { - // let's make sure there are actually .osu files to import. - string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)); - - if (string.IsNullOrEmpty(mapName)) - { - Logger.Log($"No beatmap files found in the beatmap archive ({reader.Name}).", LoggingTarget.Database); - return null; - } - - Beatmap beatmap; - using (var stream = new LineBufferedReader(reader.GetStream(mapName))) - beatmap = Decoder.GetDecoder(stream).Decode(stream); - - return new BeatmapSetInfo - { - OnlineBeatmapSetID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID, - Beatmaps = new List(), - Metadata = beatmap.Metadata, - DateAdded = DateTimeOffset.UtcNow - }; - } - - /// - /// Create all required s for the provided archive. - /// - private List createBeatmapDifficulties(List files) - { - var beatmapInfos = new List(); - - foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase))) - { - using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath)) - using (var ms = new MemoryStream()) // we need a memory stream so we can seek - using (var sr = new LineBufferedReader(ms)) - { - raw.CopyTo(ms); - ms.Position = 0; - - var decoder = Decoder.GetDecoder(sr); - IBeatmap beatmap = decoder.Decode(sr); - - string hash = ms.ComputeSHA2Hash(); - - if (beatmapInfos.Any(b => b.Hash == hash)) - continue; - - beatmap.BeatmapInfo.Path = file.Filename; - beatmap.BeatmapInfo.Hash = hash; - beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash(); - - var ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); - beatmap.BeatmapInfo.Ruleset = ruleset; - - // TODO: this should be done in a better place once we actually need to dynamically update it. - beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0; - beatmap.BeatmapInfo.Length = calculateLength(beatmap); - beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); - - beatmapInfos.Add(beatmap.BeatmapInfo); - } - } - - return beatmapInfos; - } - - private double calculateLength(IBeatmap b) - { - if (!b.HitObjects.Any()) - return 0; - - var lastObject = b.HitObjects.Last(); - - //TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list). - double endTime = lastObject.GetEndTime(); - double startTime = b.HitObjects.First().StartTime; - - return endTime - startTime; - } - - /// - /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. - /// - private class DummyConversionBeatmap : WorkingBeatmap - { - private readonly IBeatmap beatmap; - - public DummyConversionBeatmap(IBeatmap beatmap) - : base(beatmap.BeatmapInfo, null) - { - this.beatmap = beatmap; - } - - protected override IBeatmap GetBeatmap() => beatmap; - protected override Texture GetBackground() => null; - protected override Track GetBeatmapTrack() => null; - protected internal override ISkin GetSkin() => null; - public override Stream GetStream(string storagePath) => null; - } } - /// - /// The level of detail to include in database results. - /// - public enum IncludedDetails - { - /// - /// Only include beatmap difficulties and set level metadata. - /// - Minimal, - - /// - /// Include all difficulties, rulesets, difficulty metadata but no files. - /// - AllButFiles, - - /// - /// Include everything except ruleset. Used for cases where we aren't sure the ruleset is present but still want to consume the beatmap. - /// - AllButRuleset, - - /// - /// Include everything. - /// - All - } } diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs new file mode 100644 index 0000000000..be3adc412c --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -0,0 +1,479 @@ +// 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.Linq.Expressions; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics.Textures; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps.Formats; +using osu.Game.Database; +using osu.Game.IO; +using osu.Game.IO.Archives; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Skinning; +using Decoder = osu.Game.Beatmaps.Formats.Decoder; + +namespace osu.Game.Beatmaps +{ + /// + /// Handles ef-core storage of beatmaps. + /// + [ExcludeFromDynamicCompile] + public class BeatmapModelManager : DownloadableArchiveModelManager + { + /// + /// Fired when a single difficulty has been hidden. + /// + public IBindable> BeatmapHidden => beatmapHidden; + + private readonly Bindable> beatmapHidden = new Bindable>(); + + /// + /// Fired when a single difficulty has been restored. + /// + public IBindable> BeatmapRestored => beatmapRestored; + + /// + /// A function which populates online information during the import process. + /// It is run as the final step of import. + /// + public Func PopulateOnlineInformation; + + /// + /// The game working beatmap cache, used to invalidate entries on changes. + /// + public WorkingBeatmapCache WorkingBeatmapCache { private get; set; } + + private readonly Bindable> beatmapRestored = new Bindable>(); + + public override IEnumerable HandledExtensions => new[] { ".osz" }; + + protected override string[] HashableFileTypes => new[] { ".osu" }; + + protected override string ImportFromStablePath => "."; + + protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage(); + + private readonly BeatmapStore beatmaps; + private readonly RulesetStore rulesets; + + public BeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host = null) + : base(storage, contextFactory, api, new BeatmapStore(contextFactory), host) + { + this.rulesets = rulesets; + + beatmaps = (BeatmapStore)ModelStore; + beatmaps.BeatmapHidden += b => beatmapHidden.Value = new WeakReference(b); + beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference(b); + beatmaps.ItemRemoved += b => WorkingBeatmapCache?.Invalidate(b); + beatmaps.ItemUpdated += obj => WorkingBeatmapCache?.Invalidate(obj); + } + + protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => + new DownloadBeatmapSetRequest(set, minimiseDownloadSize); + + protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; + + protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) + { + if (archive != null) + beatmapSet.Beatmaps = createBeatmapDifficulties(beatmapSet.Files); + + foreach (BeatmapInfo b in beatmapSet.Beatmaps) + { + // remove metadata from difficulties where it matches the set + if (beatmapSet.Metadata.Equals(b.Metadata)) + b.Metadata = null; + + b.BeatmapSet = beatmapSet; + } + + validateOnlineIds(beatmapSet); + + bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); + + if (PopulateOnlineInformation != null) + await PopulateOnlineInformation(beatmapSet, cancellationToken).ConfigureAwait(false); + + // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. + if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) + { + if (beatmapSet.OnlineBeatmapSetID != null) + { + beatmapSet.OnlineBeatmapSetID = null; + LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs"); + } + } + } + + protected override void PreImport(BeatmapSetInfo beatmapSet) + { + if (beatmapSet.Beatmaps.Any(b => b.BaseDifficulty == null)) + throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}."); + + // check if a set already exists with the same online id, delete if it does. + if (beatmapSet.OnlineBeatmapSetID != null) + { + var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID); + + if (existingOnlineId != null) + { + Delete(existingOnlineId); + + // in order to avoid a unique key constraint, immediately remove the online ID from the previous set. + existingOnlineId.OnlineBeatmapSetID = null; + foreach (var b in existingOnlineId.Beatmaps) + b.OnlineBeatmapID = null; + + LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been deleted."); + } + } + } + + private void validateOnlineIds(BeatmapSetInfo beatmapSet) + { + var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList(); + + // ensure all IDs are unique + if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1)) + { + LogForModel(beatmapSet, "Found non-unique IDs, resetting..."); + resetIds(); + return; + } + + // find any existing beatmaps in the database that have matching online ids + var existingBeatmaps = QueryBeatmaps(b => beatmapIds.Contains(b.OnlineBeatmapID)).ToList(); + + if (existingBeatmaps.Count > 0) + { + // reset the import ids (to force a re-fetch) *unless* they match the candidate CheckForExisting set. + // we can ignore the case where the new ids are contained by the CheckForExisting set as it will either be used (import skipped) or deleted. + var existing = CheckForExisting(beatmapSet); + + if (existing == null || existingBeatmaps.Any(b => !existing.Beatmaps.Contains(b))) + { + LogForModel(beatmapSet, "Found existing import with IDs already, resetting..."); + resetIds(); + } + } + + void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null); + } + + protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable items) + => base.CheckLocalAvailability(model, items) + || (model.OnlineBeatmapSetID != null && items.Any(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID)); + + /// + /// Delete a beatmap difficulty. + /// + /// The beatmap difficulty to hide. + public void Hide(BeatmapInfo beatmap) => beatmaps.Hide(beatmap); + + /// + /// Restore a beatmap difficulty. + /// + /// The beatmap difficulty to restore. + public void Restore(BeatmapInfo beatmap) => beatmaps.Restore(beatmap); + + /// + /// Saves an file against a given . + /// + /// The to save the content against. The file referenced by will be replaced. + /// The content to write. + /// The beatmap content to write, null if to be omitted. + public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) + { + var setInfo = info.BeatmapSet; + + using (var stream = new MemoryStream()) + { + using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) + new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); + + stream.Seek(0, SeekOrigin.Begin); + + using (ContextFactory.GetForWrite()) + { + var beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == info.ID); + var metadata = beatmapInfo.Metadata ?? setInfo.Metadata; + + // grab the original file (or create a new one if not found). + var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo(); + + // metadata may have changed; update the path with the standard format. + beatmapInfo.Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.Version}].osu"; + beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); + + // update existing or populate new file's filename. + fileInfo.Filename = beatmapInfo.Path; + + stream.Seek(0, SeekOrigin.Begin); + ReplaceFile(setInfo, fileInfo, stream); + } + } + + WorkingBeatmapCache?.Invalidate(info); + } + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The first result for the provided query, or null if no results were found. + public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query); + + protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import) + { + if (!base.CanSkipImport(existing, import)) + return false; + + return existing.Beatmaps.Any(b => b.OnlineBeatmapID != null); + } + + protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import) + { + if (!base.CanReuseExisting(existing, import)) + return false; + + var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); + var importIds = import.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i); + + // force re-import if we are not in a sane state. + return existing.OnlineBeatmapSetID == import.OnlineBeatmapSetID && existingIds.SequenceEqual(importIds); + } + + /// + /// Returns a list of all usable s. + /// + /// A list of available . + public List GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) => + GetAllUsableBeatmapSetsEnumerable(includes, includeProtected).ToList(); + + /// + /// Returns a list of all usable s. Note that files are not populated. + /// + /// The level of detail to include in the returned objects. + /// Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases. + /// A list of available . + public IEnumerable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false) + { + IQueryable queryable; + + switch (includes) + { + case IncludedDetails.Minimal: + queryable = beatmaps.BeatmapSetsOverview; + break; + + case IncludedDetails.AllButRuleset: + queryable = beatmaps.BeatmapSetsWithoutRuleset; + break; + + case IncludedDetails.AllButFiles: + queryable = beatmaps.BeatmapSetsWithoutFiles; + break; + + default: + queryable = beatmaps.ConsumableItems; + break; + } + + // AsEnumerable used here to avoid applying the WHERE in sql. When done so, ef core 2.x uses an incorrect ORDER BY + // clause which causes queries to take 5-10x longer. + // TODO: remove if upgrading to EF core 3.x. + return queryable.AsEnumerable().Where(s => !s.DeletePending && (includeProtected || !s.Protected)); + } + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The level of detail to include in the returned objects. + /// Results from the provided query. + public IEnumerable QueryBeatmapSets(Expression> query, IncludedDetails includes = IncludedDetails.All) + { + IQueryable queryable; + + switch (includes) + { + case IncludedDetails.Minimal: + queryable = beatmaps.BeatmapSetsOverview; + break; + + case IncludedDetails.AllButRuleset: + queryable = beatmaps.BeatmapSetsWithoutRuleset; + break; + + case IncludedDetails.AllButFiles: + queryable = beatmaps.BeatmapSetsWithoutFiles; + break; + + default: + queryable = beatmaps.ConsumableItems; + break; + } + + return queryable.AsNoTracking().Where(query); + } + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The first result for the provided query, or null if no results were found. + public BeatmapInfo QueryBeatmap(Expression> query) => beatmaps.Beatmaps.AsNoTracking().FirstOrDefault(query); + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// Results from the provided query. + public IQueryable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); + + protected override string HumanisedModelName => "beatmap"; + + protected override BeatmapSetInfo CreateModel(ArchiveReader reader) + { + // let's make sure there are actually .osu files to import. + string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)); + + if (string.IsNullOrEmpty(mapName)) + { + Logger.Log($"No beatmap files found in the beatmap archive ({reader.Name}).", LoggingTarget.Database); + return null; + } + + Beatmap beatmap; + using (var stream = new LineBufferedReader(reader.GetStream(mapName))) + beatmap = Decoder.GetDecoder(stream).Decode(stream); + + return new BeatmapSetInfo + { + OnlineBeatmapSetID = beatmap.BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID, + Beatmaps = new List(), + Metadata = beatmap.Metadata, + DateAdded = DateTimeOffset.UtcNow + }; + } + + /// + /// Create all required s for the provided archive. + /// + private List createBeatmapDifficulties(List files) + { + var beatmapInfos = new List(); + + foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase))) + { + using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath)) + using (var ms = new MemoryStream()) // we need a memory stream so we can seek + using (var sr = new LineBufferedReader(ms)) + { + raw.CopyTo(ms); + ms.Position = 0; + + var decoder = Decoder.GetDecoder(sr); + IBeatmap beatmap = decoder.Decode(sr); + + string hash = ms.ComputeSHA2Hash(); + + if (beatmapInfos.Any(b => b.Hash == hash)) + continue; + + beatmap.BeatmapInfo.Path = file.Filename; + beatmap.BeatmapInfo.Hash = hash; + beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash(); + + var ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); + beatmap.BeatmapInfo.Ruleset = ruleset; + + // TODO: this should be done in a better place once we actually need to dynamically update it. + beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0; + beatmap.BeatmapInfo.Length = calculateLength(beatmap); + beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); + + beatmapInfos.Add(beatmap.BeatmapInfo); + } + } + + return beatmapInfos; + } + + private double calculateLength(IBeatmap b) + { + if (!b.HitObjects.Any()) + return 0; + + var lastObject = b.HitObjects.Last(); + + //TODO: this isn't always correct (consider mania where a non-last object may last for longer than the last in the list). + double endTime = lastObject.GetEndTime(); + double startTime = b.HitObjects.First().StartTime; + + return endTime - startTime; + } + + /// + /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. + /// + private class DummyConversionBeatmap : WorkingBeatmap + { + private readonly IBeatmap beatmap; + + public DummyConversionBeatmap(IBeatmap beatmap) + : base(beatmap.BeatmapInfo, null) + { + this.beatmap = beatmap; + } + + protected override IBeatmap GetBeatmap() => beatmap; + protected override Texture GetBackground() => null; + protected override Track GetBeatmapTrack() => null; + protected internal override ISkin GetSkin() => null; + public override Stream GetStream(string storagePath) => null; + } + } + + /// + /// The level of detail to include in database results. + /// + public enum IncludedDetails + { + /// + /// Only include beatmap difficulties and set level metadata. + /// + Minimal, + + /// + /// Include all difficulties, rulesets, difficulty metadata but no files. + /// + AllButFiles, + + /// + /// Include everything except ruleset. Used for cases where we aren't sure the ruleset is present but still want to consume the beatmap. + /// + AllButRuleset, + + /// + /// Include everything. + /// + All + } +} From 5618c9933bfd61b5587160a1821248bf7b1fb214 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 16:44:39 +0900 Subject: [PATCH 091/391] Expose more pieces of `ArchiveModelManager` via interfaces --- osu.Game/Database/ArchiveModelManager.cs | 11 +- .../DownloadableArchiveModelManager.cs | 6 -- osu.Game/Database/IModelFileManager.cs | 36 +++++++ osu.Game/Database/IModelManager.cs | 101 +++++++++++++++++- osu.Game/Scoring/ScoreManager.cs | 2 +- 5 files changed, 141 insertions(+), 15 deletions(-) create mode 100644 osu.Game/Database/IModelFileManager.cs diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index ddd2bc5d1e..fc217d3058 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -30,7 +30,7 @@ namespace osu.Game.Database /// /// The model type. /// The associated file join type. - public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager + public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete where TFileModel : class, INamedFileInfo, new() { @@ -135,7 +135,7 @@ namespace osu.Game.Database return Import(notification, tasks); } - protected async Task> Import(ProgressNotification notification, params ImportTask[] tasks) + public async Task> Import(ProgressNotification notification, params ImportTask[] tasks) { if (tasks.Length == 0) { @@ -227,7 +227,7 @@ namespace osu.Game.Database /// Whether this is a low priority import. /// An optional cancellation token. /// The imported model, if successful. - internal async Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + public async Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -479,7 +479,7 @@ namespace osu.Game.Database /// /// The item to export. /// The output stream to export to. - protected virtual void ExportModelTo(TModel model, Stream outputStream) + public virtual void ExportModelTo(TModel model, Stream outputStream) { using (var archive = ZipArchive.Create()) { @@ -745,9 +745,6 @@ namespace osu.Game.Database /// Whether to perform deletion. protected virtual bool ShouldDeleteArchive(string path) => false; - /// - /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. - /// public Task ImportFromStableAsync(StableStorage stableStorage) { var storage = PrepareStableStorage(stableStorage); diff --git a/osu.Game/Database/DownloadableArchiveModelManager.cs b/osu.Game/Database/DownloadableArchiveModelManager.cs index da3144e8d0..e6d5b44b65 100644 --- a/osu.Game/Database/DownloadableArchiveModelManager.cs +++ b/osu.Game/Database/DownloadableArchiveModelManager.cs @@ -54,12 +54,6 @@ namespace osu.Game.Database /// The request object. protected abstract ArchiveDownloadRequest CreateDownloadRequest(TModel model, bool minimiseDownloadSize); - /// - /// Begin a download for the requested . - /// - /// The to be downloaded. - /// Whether this download should be optimised for slow connections. Generally means extras are not included in the download bundle. - /// Whether the download was started. public bool Download(TModel model, bool minimiseDownloadSize = false) { if (!canDownload(model)) return false; diff --git a/osu.Game/Database/IModelFileManager.cs b/osu.Game/Database/IModelFileManager.cs new file mode 100644 index 0000000000..c74b945eb7 --- /dev/null +++ b/osu.Game/Database/IModelFileManager.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 System.IO; + +namespace osu.Game.Database +{ + public interface IModelFileManager + where TModel : class + where TFileModel : class + { + /// + /// Replace an existing file with a new version. + /// + /// The item to operate on. + /// The existing file to be replaced. + /// The new file contents. + /// An optional filename for the new file. Will use the previous filename if not specified. + void ReplaceFile(TModel model, TFileModel file, Stream contents, string filename = null); + + /// + /// Delete an existing file. + /// + /// The item to operate on. + /// The existing file to be deleted. + void DeleteFile(TModel model, TFileModel file); + + /// + /// Add a new file. + /// + /// The item to operate on. + /// The new file contents. + /// The filename for the new file. + void AddFile(TModel model, Stream contents, string filename); + } +} diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index 8c314f1617..8f0c6e1561 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.cs @@ -1,8 +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 System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; using osu.Framework.Bindables; +using osu.Game.IO; +using osu.Game.IO.Archives; +using osu.Game.Overlays.Notifications; namespace osu.Game.Database { @@ -24,5 +31,97 @@ namespace osu.Game.Database /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. /// IBindable> ItemRemoved { get; } + + /// + /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. + /// + Task ImportFromStableAsync(StableStorage stableStorage); + + /// + /// Exports an item to a legacy (.zip based) package. + /// + /// The item to export. + void Export(TModel item); + + /// + /// Exports an item to the given output stream. + /// + /// The item to export. + /// The output stream to export to. + void ExportModelTo(TModel model, Stream outputStream); + + /// + /// Perform an update of the specified item. + /// TODO: Support file additions/removals. + /// + /// The item to update. + void Update(TModel item); + + /// + /// Delete an item from the manager. + /// Is a no-op for already deleted items. + /// + /// The item to delete. + /// false if no operation was performed + bool Delete(TModel item); + + /// + /// Delete multiple items. + /// This will post notifications tracking progress. + /// + void Delete(List items, bool silent = false); + + /// + /// Restore multiple items that were previously deleted. + /// This will post notifications tracking progress. + /// + void Undelete(List items, bool silent = false); + + /// + /// Restore an item that was previously deleted. Is a no-op if the item is not in a deleted state, or has its protected flag set. + /// + /// The item to restore + void Undelete(TModel item); + + /// + /// Import one or more items from filesystem . + /// + /// + /// This will be treated as a low priority import if more than one path is specified; use to always import at standard priority. + /// This will post notifications tracking progress. + /// + /// One or more archive locations on disk. + Task Import(params string[] paths); + + Task Import(params ImportTask[] tasks); + + Task> Import(ProgressNotification notification, params ImportTask[] tasks); + + /// + /// 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. + /// + /// The containing data about the to import. + /// Whether this is a low priority import. + /// An optional cancellation token. + /// The imported model, if successful. + Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default); + + /// + /// Silently import an item from an . + /// + /// The archive to be imported. + /// Whether this is a low priority import. + /// An optional cancellation token. + Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default); + + /// + /// Silently import an item from a . + /// + /// The model to be imported. + /// An optional archive to use for model population. + /// Whether this is a low priority import. + /// An optional cancellation token. + Task Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); } } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 81e701f001..56c346d177 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -78,7 +78,7 @@ namespace osu.Game.Scoring protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) => Task.CompletedTask; - protected override void ExportModelTo(ScoreInfo model, Stream outputStream) + public override void ExportModelTo(ScoreInfo model, Stream outputStream) { var file = model.Files.SingleOrDefault(); if (file == null) From 84bddf0885893d700addf2cbc00900dc408a0f8e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 17:00:15 +0900 Subject: [PATCH 092/391] Initial PP counter implementation --- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 +- .../Difficulty/DifficultyCalculator.cs | 126 +++++++++++++++--- .../HUD/DefaultPerformancePointsCounter.cs | 108 +++++++++++++++ osu.Game/Screens/Play/Player.cs | 8 +- 4 files changed, 218 insertions(+), 26 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 61760e69b0..c4c5c89f28 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -81,7 +81,7 @@ namespace osu.Game.Beatmaps /// The applicable . protected virtual IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) => ruleset.CreateBeatmapConverter(beatmap); - public IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null) + public virtual IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null) { using (var cancellationSource = createCancellationTokenSource(timeout)) { diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 224c9178ae..f38949d982 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -7,6 +7,8 @@ using System.Linq; using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; @@ -19,6 +21,10 @@ namespace osu.Game.Rulesets.Difficulty private readonly Ruleset ruleset; private readonly WorkingBeatmap beatmap; + private IBeatmap playableBeatmap; + private Mod[] playableMods; + private double clockRate; + protected DifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) { this.ruleset = ruleset; @@ -32,14 +38,41 @@ namespace osu.Game.Rulesets.Difficulty /// A structure describing the difficulty of the beatmap. public DifficultyAttributes Calculate(params Mod[] mods) { - mods = mods.Select(m => m.DeepClone()).ToArray(); + preProcess(mods); - IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods); + var skills = CreateSkills(playableBeatmap, playableMods, clockRate); - var track = new TrackVirtual(10000); - mods.OfType().ForEach(m => m.ApplyToTrack(track)); + if (!playableBeatmap.HitObjects.Any()) + return CreateDifficultyAttributes(playableBeatmap, playableMods, skills, clockRate); - return calculate(playableBeatmap, mods, track.Rate); + foreach (var hitObject in getDifficultyHitObjects()) + { + foreach (var skill in skills) + skill.ProcessInternal(hitObject); + } + + return CreateDifficultyAttributes(playableBeatmap, playableMods, skills, clockRate); + } + + public IEnumerable CalculateTimed(params Mod[] mods) + { + preProcess(mods); + + if (!playableBeatmap.HitObjects.Any()) + yield break; + + var skills = CreateSkills(playableBeatmap, playableMods, clockRate); + var progressiveBeatmap = new ProgressiveCalculationBeatmap(playableBeatmap); + + foreach (var hitObject in getDifficultyHitObjects()) + { + progressiveBeatmap.HitObjects.Add(hitObject.BaseObject); + + foreach (var skill in skills) + skill.ProcessInternal(hitObject); + + yield return new TimedDifficultyAttributes(hitObject.EndTime, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate)); + } } /// @@ -57,24 +90,23 @@ namespace osu.Game.Rulesets.Difficulty } } - private DifficultyAttributes calculate(IBeatmap beatmap, Mod[] mods, double clockRate) + /// + /// Retrieves the s to calculate against. + /// + private IEnumerable getDifficultyHitObjects() => SortObjects(CreateDifficultyHitObjects(playableBeatmap, clockRate)); + + /// + /// Performs required tasks before every calculation. + /// + /// The original list of s. + private void preProcess(Mod[] mods) { - var skills = CreateSkills(beatmap, mods, clockRate); + playableMods = mods.Select(m => m.DeepClone()).ToArray(); + playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods); - if (!beatmap.HitObjects.Any()) - return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); - - var difficultyHitObjects = SortObjects(CreateDifficultyHitObjects(beatmap, clockRate)).ToList(); - - foreach (var hitObject in difficultyHitObjects) - { - foreach (var skill in skills) - { - skill.ProcessInternal(hitObject); - } - } - - return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); + var track = new TrackVirtual(10000); + mods.OfType().ForEach(m => m.ApplyToTrack(track)); + clockRate = track.Rate; } /// @@ -183,5 +215,57 @@ namespace osu.Game.Rulesets.Difficulty /// Clockrate to calculate difficulty with. /// The s. protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate); + + public class TimedDifficultyAttributes : IComparable + { + public readonly double Time; + public readonly DifficultyAttributes Attributes; + + public TimedDifficultyAttributes(double time, DifficultyAttributes attributes) + { + Time = time; + Attributes = attributes; + } + + public int CompareTo(TimedDifficultyAttributes other) => Time.CompareTo(other.Time); + } + + private class ProgressiveCalculationBeatmap : IBeatmap + { + private readonly IBeatmap baseBeatmap; + + public ProgressiveCalculationBeatmap(IBeatmap baseBeatmap) + { + this.baseBeatmap = baseBeatmap; + } + + public BeatmapInfo BeatmapInfo + { + get => baseBeatmap.BeatmapInfo; + set => baseBeatmap.BeatmapInfo = value; + } + + public BeatmapMetadata Metadata => baseBeatmap.Metadata; + + public ControlPointInfo ControlPointInfo + { + get => baseBeatmap.ControlPointInfo; + set => baseBeatmap.ControlPointInfo = value; + } + + public List Breaks => baseBeatmap.Breaks; + + public double TotalBreakTime => baseBeatmap.TotalBreakTime; + + public readonly List HitObjects = new List(); + + IReadOnlyList IBeatmap.HitObjects => HitObjects; + + public IEnumerable GetStatistics() => baseBeatmap.GetStatistics(); + + public double GetMostCommonBeatLength() => baseBeatmap.GetMostCommonBeatLength(); + + public IBeatmap Clone() => new ProgressiveCalculationBeatmap(baseBeatmap.Clone()); + } } } diff --git a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs new file mode 100644 index 0000000000..18bb621dd1 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.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 System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; +using osu.Framework.Localisation; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; + +namespace osu.Game.Screens.Play.HUD +{ + public class DefaultPerformancePointsCounter : RollingCounter, ISkinnableDrawable + { + public bool UsesFixedAnchor { get; set; } + + [Resolved] + private ScoreProcessor scoreProcessor { get; set; } + + [Resolved] + private Player player { get; set; } + + private DifficultyCalculator.TimedDifficultyAttributes[] timedAttributes; + private Ruleset gameplayRuleset; + + public DefaultPerformancePointsCounter() + { + Current.Value = DisplayedCount = 0; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.BlueLighter; + + gameplayRuleset = player.GameplayRuleset; + timedAttributes = gameplayRuleset.CreateDifficultyCalculator(new GameplayWorkingBeatmap(player.GameplayBeatmap)).CalculateTimed(player.Mods.Value.ToArray()).ToArray(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + scoreProcessor.NewJudgement += onNewJudgement; + } + + private void onNewJudgement(JudgementResult judgement) + { + var attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new DifficultyCalculator.TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); + if (attribIndex < 0) + attribIndex = ~attribIndex - 1; + attribIndex = Math.Clamp(attribIndex, 0, timedAttributes.Length - 1); + + var ppProcessor = gameplayRuleset.CreatePerformanceCalculator(timedAttributes[attribIndex].Attributes, player.Score.ScoreInfo); + Current.Value = (int)(ppProcessor?.Calculate() ?? 0); + } + + protected override LocalisableString FormatCount(int count) => $@"{count}pp"; + + protected override OsuSpriteText CreateSpriteText() + => base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f)); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (scoreProcessor != null) + scoreProcessor.NewJudgement -= onNewJudgement; + } + + private class GameplayWorkingBeatmap : WorkingBeatmap + { + private readonly GameplayBeatmap gameplayBeatmap; + + public GameplayWorkingBeatmap(GameplayBeatmap gameplayBeatmap) + : base(gameplayBeatmap.BeatmapInfo, null) + { + this.gameplayBeatmap = gameplayBeatmap; + } + + public override IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null) + => gameplayBeatmap; + + protected override IBeatmap GetBeatmap() => gameplayBeatmap; + + protected override Texture GetBackground() => throw new NotImplementedException(); + + protected override Track GetBeatmapTrack() => throw new NotImplementedException(); + + protected internal override ISkin GetSkin() => throw new NotImplementedException(); + + public override Stream GetStream(string storagePath) => throw new NotImplementedException(); + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9927467bd6..4018650093 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -93,9 +93,9 @@ namespace osu.Game.Screens.Play [Resolved] private SpectatorClient spectatorClient { get; set; } - protected Ruleset GameplayRuleset { get; private set; } + public Ruleset GameplayRuleset { get; private set; } - protected GameplayBeatmap GameplayBeatmap { get; private set; } + public GameplayBeatmap GameplayBeatmap { get; private set; } private Sample sampleRestart; @@ -127,7 +127,7 @@ namespace osu.Game.Screens.Play [Cached] [Cached(Type = typeof(IBindable>))] - protected new readonly Bindable> Mods = new Bindable>(Array.Empty()); + public new readonly Bindable> Mods = new Bindable>(Array.Empty()); /// /// Whether failing should be allowed. @@ -137,7 +137,7 @@ namespace osu.Game.Screens.Play public readonly PlayerConfiguration Configuration; - protected Score Score { get; private set; } + public Score Score { get; private set; } /// /// Create a new player instance. From 90225f20820ed74fe6269a7c1c4105d2c3e4866a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 16:45:32 +0900 Subject: [PATCH 093/391] Hook up all required interfaces to new `BeatmapManager` --- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 26 +- osu.Game/Beatmaps/BeatmapManager.cs | 297 +++++++++++++++++- osu.Game/Beatmaps/IWorkingBeatmapCache.cs | 15 + osu.Game/Beatmaps/WorkingBeatmapCache.cs | 42 +-- osu.Game/OsuGameBase.cs | 6 - osu.Game/Tests/Visual/EditorTestScene.cs | 37 ++- 6 files changed, 364 insertions(+), 59 deletions(-) create mode 100644 osu.Game/Beatmaps/IWorkingBeatmapCache.cs diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index a7d34fadbe..1a3f9e414d 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -158,18 +158,34 @@ namespace osu.Game.Tests.Online public Task CurrentImportTask { get; private set; } - protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) - => new TestDownloadRequest(set); + protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) + { + return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, api, host); + } public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap) { } - public override async Task Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + internal class TestBeatmapModelManager : BeatmapModelManager { - await AllowImport.Task.ConfigureAwait(false); - return await (CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)).ConfigureAwait(false); + private readonly TestBeatmapManager testBeatmapManager; + + public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost) + : base(storage, databaseContextFactory, rulesetStore, apiProvider, gameHost) + { + this.testBeatmapManager = testBeatmapManager; + } + + protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) + => new TestDownloadRequest(set); + + public override async Task Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + { + await testBeatmapManager.AllowImport.Task.ConfigureAwait(false); + return await (testBeatmapManager.CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)).ConfigureAwait(false); + } } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index c445925a90..18513945e5 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -1,7 +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 System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using osu.Framework.Audio; +using osu.Framework.Bindables; +using osu.Framework.IO.Stores; +using osu.Framework.Platform; using osu.Framework.Testing; +using osu.Game.Database; +using osu.Game.IO; +using osu.Game.IO.Archives; +using osu.Game.Online.API; +using osu.Game.Overlays.Notifications; +using osu.Game.Rulesets; +using osu.Game.Skinning; +using osu.Game.Users; namespace osu.Game.Beatmaps { @@ -9,13 +29,282 @@ namespace osu.Game.Beatmaps /// Handles general operations related to global beatmap management. /// [ExcludeFromDynamicCompile] - public class BeatmapManager + public class BeatmapManager : IModelDownloader, IModelFileManager, ICanAcceptFiles, IWorkingBeatmapCache { - public BeatmapManager() + private readonly BeatmapModelManager beatmapModelManager; + private readonly WorkingBeatmapCache workingBeatmapCache; + + public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, + WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) { - beatmapModelManager = new BeatmapModelManager() + beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, api, host); + workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, resources, new FileStore(contextFactory, storage).Store, defaultBeatmap, host); + + workingBeatmapCache.BeatmapManager = beatmapModelManager; + + var onlineBeatmapLookupCache = new BeatmapOnlineLookupQueue(api, storage); + + beatmapModelManager.PopulateOnlineInformation = onlineBeatmapLookupCache.UpdateAsync; } - } + protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost host) => + new WorkingBeatmapCache(audioManager, resources, storage, defaultBeatmap, host); + protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) => + new BeatmapModelManager(storage, contextFactory, rulesets, api, host); + + /// + /// Create a new . + /// + public WorkingBeatmap CreateNew(RulesetInfo ruleset, User user) + { + var metadata = new BeatmapMetadata + { + Author = user, + }; + + var set = new BeatmapSetInfo + { + Metadata = metadata, + Beatmaps = new List + { + new BeatmapInfo + { + BaseDifficulty = new BeatmapDifficulty(), + Ruleset = ruleset, + Metadata = metadata, + WidescreenStoryboard = true, + SamplesMatchPlaybackRate = true, + } + } + }; + + var working = beatmapModelManager.Import(set).Result; + return GetWorkingBeatmap(working.Beatmaps.First()); + } + + #region Delegation to BeatmapModelManager (methods which previously existed locally). + + /// + /// Fired when a single difficulty has been hidden. + /// + public IBindable> BeatmapHidden => beatmapModelManager.BeatmapHidden; + + /// + /// Fired when a single difficulty has been restored. + /// + public IBindable> BeatmapRestored => beatmapModelManager.BeatmapRestored; + + /// + /// Saves an file against a given . + /// + /// The to save the content against. The file referenced by will be replaced. + /// The content to write. + /// The beatmap content to write, null if to be omitted. + public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) => beatmapModelManager.Save(info, beatmapContent, beatmapSkin); + + /// + /// Returns a list of all usable s. + /// + /// A list of available . + public List GetAllUsableBeatmapSets(IncludedDetails includes = IncludedDetails.All, bool includeProtected = false) => beatmapModelManager.GetAllUsableBeatmapSets(includes, includeProtected); + + /// + /// Returns a list of all usable s. Note that files are not populated. + /// + /// The level of detail to include in the returned objects. + /// Whether to include protected (system) beatmaps. These should not be included for gameplay playable use cases. + /// A list of available . + public IEnumerable GetAllUsableBeatmapSetsEnumerable(IncludedDetails includes, bool includeProtected = false) => beatmapModelManager.GetAllUsableBeatmapSetsEnumerable(includes, includeProtected); + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The level of detail to include in the returned objects. + /// Results from the provided query. + public IEnumerable QueryBeatmapSets(Expression> query, IncludedDetails includes = IncludedDetails.All) => beatmapModelManager.QueryBeatmapSets(query, includes); + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The first result for the provided query, or null if no results were found. + public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmapModelManager.QueryBeatmapSet(query); + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// Results from the provided query. + public IQueryable QueryBeatmaps(Expression> query) => beatmapModelManager.QueryBeatmaps(query); + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The first result for the provided query, or null if no results were found. + public BeatmapInfo QueryBeatmap(Expression> query) => beatmapModelManager.QueryBeatmap(query); + + /// + /// A default representation of a WorkingBeatmap to use when no beatmap is available. + /// + public WorkingBeatmap DefaultBeatmap => workingBeatmapCache.DefaultBeatmap; + + /// + /// Fired when a notification should be presented to the user. + /// + public Action PostNotification { set => beatmapModelManager.PostNotification = value; } + + /// + /// Fired when the user requests to view the resulting import. + /// + public Action> PresentImport { set => beatmapModelManager.PresentImport = value; } + + /// + /// Delete a beatmap difficulty. + /// + /// The beatmap difficulty to hide. + public void Hide(BeatmapInfo beatmap) => beatmapModelManager.Hide(beatmap); + + /// + /// Restore a beatmap difficulty. + /// + /// The beatmap difficulty to restore. + public void Restore(BeatmapInfo beatmap) => beatmapModelManager.Restore(beatmap); + + #endregion + + #region Implementation of IModelManager + + public IBindable> ItemUpdated => beatmapModelManager.ItemUpdated; + + public IBindable> ItemRemoved => beatmapModelManager.ItemRemoved; + + public Task ImportFromStableAsync(StableStorage stableStorage) + { + return beatmapModelManager.ImportFromStableAsync(stableStorage); + } + + public void Export(BeatmapSetInfo item) + { + beatmapModelManager.Export(item); + } + + public void ExportModelTo(BeatmapSetInfo model, Stream outputStream) + { + beatmapModelManager.ExportModelTo(model, outputStream); + } + + public void Update(BeatmapSetInfo item) + { + beatmapModelManager.Update(item); + } + + public bool Delete(BeatmapSetInfo item) + { + return beatmapModelManager.Delete(item); + } + + public void Delete(List items, bool silent = false) + { + beatmapModelManager.Delete(items, silent); + } + + public void Undelete(List items, bool silent = false) + { + beatmapModelManager.Undelete(items, silent); + } + + public void Undelete(BeatmapSetInfo item) + { + beatmapModelManager.Undelete(item); + } + + #endregion + + #region Implementation of IModelDownloader + + public IBindable>> DownloadBegan => beatmapModelManager.DownloadBegan; + + public IBindable>> DownloadFailed => beatmapModelManager.DownloadFailed; + + public bool IsAvailableLocally(BeatmapSetInfo model) + { + return beatmapModelManager.IsAvailableLocally(model); + } + + public bool Download(BeatmapSetInfo model, bool minimiseDownloadSize = false) + { + return beatmapModelManager.Download(model, minimiseDownloadSize); + } + + public ArchiveDownloadRequest GetExistingDownload(BeatmapSetInfo model) + { + return beatmapModelManager.GetExistingDownload(model); + } + + #endregion + + #region Implementation of ICanAcceptFiles + + public Task Import(params string[] paths) + { + return beatmapModelManager.Import(paths); + } + + public Task Import(params ImportTask[] tasks) + { + return beatmapModelManager.Import(tasks); + } + + public Task> Import(ProgressNotification notification, params ImportTask[] tasks) + { + return beatmapModelManager.Import(notification, tasks); + } + + public Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return beatmapModelManager.Import(task, lowPriority, cancellationToken); + } + + public Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return beatmapModelManager.Import(archive, lowPriority, cancellationToken); + } + + public Task Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return beatmapModelManager.Import(item, archive, lowPriority, cancellationToken); + } + + public IEnumerable HandledExtensions => beatmapModelManager.HandledExtensions; + + #endregion + + #region Implementation of IWorkingBeatmapCache + + public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo importedBeatmap) => workingBeatmapCache.GetWorkingBeatmap(importedBeatmap); + + #endregion + + #region Implementation of IModelFileManager + + public void ReplaceFile(BeatmapSetInfo model, BeatmapSetFileInfo file, Stream contents, string filename = null) + { + beatmapModelManager.ReplaceFile(model, file, contents, filename); + } + + public void DeleteFile(BeatmapSetInfo model, BeatmapSetFileInfo file) + { + beatmapModelManager.DeleteFile(model, file); + } + + public void AddFile(BeatmapSetInfo model, Stream contents, string filename) + { + beatmapModelManager.AddFile(model, contents, filename); + } + + #endregion + } } diff --git a/osu.Game/Beatmaps/IWorkingBeatmapCache.cs b/osu.Game/Beatmaps/IWorkingBeatmapCache.cs new file mode 100644 index 0000000000..881e734292 --- /dev/null +++ b/osu.Game/Beatmaps/IWorkingBeatmapCache.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. + +namespace osu.Game.Beatmaps +{ + public interface IWorkingBeatmapCache + { + /// + /// Retrieve a instance for the provided + /// + /// The beatmap to lookup. + /// A instance correlating to the provided . + WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo); + } +} diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index 9f40eb4898..e117f1b82f 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.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 JetBrains.Annotations; @@ -17,14 +16,12 @@ using osu.Framework.Statistics; using osu.Framework.Testing; using osu.Game.Beatmaps.Formats; using osu.Game.IO; -using osu.Game.Rulesets; using osu.Game.Skinning; using osu.Game.Storyboards; -using osu.Game.Users; namespace osu.Game.Beatmaps { - public class WorkingBeatmapCache : IBeatmapResourceProvider + public class WorkingBeatmapCache : IBeatmapResourceProvider, IWorkingBeatmapCache { private readonly WeakList workingCache = new WeakList(); @@ -33,7 +30,7 @@ namespace osu.Game.Beatmaps /// public readonly WorkingBeatmap DefaultBeatmap; - public BeatmapManager BeatmapManager { private get; set; } + public BeatmapModelManager BeatmapManager { private get; set; } private readonly AudioManager audioManager; private readonly IResourceStore resources; @@ -74,41 +71,6 @@ namespace osu.Game.Beatmaps } } - /// - /// Create a new . - /// - public WorkingBeatmap CreateNew(RulesetInfo ruleset, User user) - { - var metadata = new BeatmapMetadata - { - Author = user, - }; - - var set = new BeatmapSetInfo - { - Metadata = metadata, - Beatmaps = new List - { - new BeatmapInfo - { - BaseDifficulty = new BeatmapDifficulty(), - Ruleset = ruleset, - Metadata = metadata, - WidescreenStoryboard = true, - SamplesMatchPlaybackRate = true, - } - } - }; - - var working = BeatmapManager.Import(set).Result; - return GetWorkingBeatmap(working.Beatmaps.First()); - } - - /// - /// Retrieve a instance for the provided - /// - /// The beatmap to lookup. - /// A instance correlating to the provided . public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) { // if there are no files, presume the full beatmap info has not yet been fetched from the database. diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8263e26dec..dc1cb7a850 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -138,8 +138,6 @@ namespace osu.Game private UserLookupCache userCache; - private BeatmapOnlineLookupQueue onlineBeatmapLookupCache; - private FileStore fileStore; private RulesetConfigCache rulesetConfigCache; @@ -246,10 +244,6 @@ namespace osu.Game dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Scheduler, Host, () => difficultyCache, LocalConfig)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap)); - onlineBeatmapLookupCache = new BeatmapOnlineLookupQueue(API, Storage); - - BeatmapManager.PopulateOnlineInformation = onlineBeatmapLookupCache.UpdateAsync; - // this should likely be moved to ArchiveModelManager when another case appears where it is necessary // to have inter-dependent model managers. this could be obtained with an IHasForeign interface to // allow lookups to be done on the child (ScoreManager in this case) to perform the cascading delete. diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 1e26036116..ac8773a840 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -123,11 +123,40 @@ namespace osu.Game.Tests.Visual this.testBeatmap = testBeatmap; } - protected override string ComputeHash(BeatmapSetInfo item, ArchiveReader reader = null) - => string.Empty; + protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) + { + return new TestBeatmapModelManager(storage, contextFactory, rulesets, api, host); + } - public override WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) - => testBeatmap; + protected override WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost host) + { + return new TestWorkingBeatmapCache(this, audioManager, resources, storage, defaultBeatmap, host); + } + + private class TestWorkingBeatmapCache : WorkingBeatmapCache + { + private readonly TestBeatmapManager testBeatmapManager; + + public TestWorkingBeatmapCache(TestBeatmapManager testBeatmapManager, AudioManager audioManager, IResourceStore resourceStore, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost gameHost) + : base(audioManager, resourceStore, storage, defaultBeatmap, gameHost) + { + this.testBeatmapManager = testBeatmapManager; + } + + public override WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo) + => testBeatmapManager.testBeatmap; + } + + internal class TestBeatmapModelManager : BeatmapModelManager + { + public TestBeatmapModelManager(Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost) + : base(storage, databaseContextFactory, rulesetStore, apiProvider, gameHost) + { + } + + protected override string ComputeHash(BeatmapSetInfo item, ArchiveReader reader = null) + => string.Empty; + } public override void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) { From 7a72747d886cc95e70c4abc94867527f2a6002e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 17:14:35 +0900 Subject: [PATCH 094/391] Add back optional online lookups --- osu.Game/Beatmaps/BeatmapManager.cs | 8 +++++--- osu.Game/OsuGameBase.cs | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 18513945e5..6ffdfa24b5 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -42,9 +42,11 @@ namespace osu.Game.Beatmaps workingBeatmapCache.BeatmapManager = beatmapModelManager; - var onlineBeatmapLookupCache = new BeatmapOnlineLookupQueue(api, storage); - - beatmapModelManager.PopulateOnlineInformation = onlineBeatmapLookupCache.UpdateAsync; + if (performOnlineLookups) + { + var onlineBeatmapLookupCache = new BeatmapOnlineLookupQueue(api, storage); + beatmapModelManager.PopulateOnlineInformation = onlineBeatmapLookupCache.UpdateAsync; + } } protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost host) => diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index dc1cb7a850..e76436a75b 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -242,7 +242,7 @@ namespace osu.Game // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Scheduler, Host, () => difficultyCache, LocalConfig)); - dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap)); + dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, performOnlineLookups: true)); // this should likely be moved to ArchiveModelManager when another case appears where it is necessary // to have inter-dependent model managers. this could be obtained with an IHasForeign interface to From d2a8f35b4c5428118d306174cb72503dd2ff66c1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 17:54:52 +0900 Subject: [PATCH 095/391] Update styling --- .../Graphics/UserInterface/RollingCounter.cs | 8 ++-- .../HUD/DefaultPerformancePointsCounter.cs | 43 +++++++++++++++++-- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index 244658b75e..f03287e7de 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -25,7 +25,7 @@ namespace osu.Game.Graphics.UserInterface set => current.Current = value; } - private SpriteText displayedCountSpriteText; + private IHasText displayedCountSpriteText; /// /// If true, the roll-up duration will be proportional to change in value. @@ -72,10 +72,10 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load() { - displayedCountSpriteText = CreateSpriteText(); + displayedCountSpriteText = CreateText(); UpdateDisplay(); - Child = displayedCountSpriteText; + Child = (Drawable)displayedCountSpriteText; } protected void UpdateDisplay() @@ -160,6 +160,8 @@ namespace osu.Game.Graphics.UserInterface this.TransformTo(nameof(DisplayedCount), newValue, rollingTotalDuration, RollingEasing); } + protected virtual IHasText CreateText() => CreateSpriteText(); + protected virtual OsuSpriteText CreateSpriteText() => new OsuSpriteText { Font = OsuFont.Numeric.With(size: 40f), diff --git a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs index 18bb621dd1..a7651187c2 100644 --- a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs @@ -68,10 +68,9 @@ namespace osu.Game.Screens.Play.HUD Current.Value = (int)(ppProcessor?.Calculate() ?? 0); } - protected override LocalisableString FormatCount(int count) => $@"{count}pp"; + protected override LocalisableString FormatCount(int count) => count.ToString(@"D"); - protected override OsuSpriteText CreateSpriteText() - => base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f)); + protected override IHasText CreateText() => new TextComponent(); protected override void Dispose(bool isDisposing) { @@ -81,6 +80,44 @@ namespace osu.Game.Screens.Play.HUD scoreProcessor.NewJudgement -= onNewJudgement; } + private class TextComponent : CompositeDrawable, IHasText + { + public LocalisableString Text + { + get => text.Text; + set => text.Text = value; + } + + private readonly OsuSpriteText text; + + public TextComponent() + { + AutoSizeAxes = Axes.Both; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(2), + Children = new Drawable[] + { + text = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.Numeric.With(size: 16) + }, + new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Text = @"pp", + Font = OsuFont.Numeric.With(size: 8) + } + } + }; + } + } + private class GameplayWorkingBeatmap : WorkingBeatmap { private readonly GameplayBeatmap gameplayBeatmap; From 4d8418e0720a43f620381142c6b05a4e1ba95a69 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 17:54:56 +0900 Subject: [PATCH 096/391] Fix possible nullrefs --- .../HUD/DefaultPerformancePointsCounter.cs | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs index a7651187c2..563032b4ea 100644 --- a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs @@ -5,9 +5,12 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Localisation; using osu.Game.Beatmaps; @@ -21,6 +24,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Screens.Play.HUD { @@ -28,10 +32,12 @@ namespace osu.Game.Screens.Play.HUD { public bool UsesFixedAnchor { get; set; } - [Resolved] + [CanBeNull] + [Resolved(CanBeNull = true)] private ScoreProcessor scoreProcessor { get; set; } - [Resolved] + [CanBeNull] + [Resolved(CanBeNull = true)] private Player player { get; set; } private DifficultyCalculator.TimedDifficultyAttributes[] timedAttributes; @@ -47,18 +53,26 @@ namespace osu.Game.Screens.Play.HUD { Colour = colours.BlueLighter; - gameplayRuleset = player.GameplayRuleset; - timedAttributes = gameplayRuleset.CreateDifficultyCalculator(new GameplayWorkingBeatmap(player.GameplayBeatmap)).CalculateTimed(player.Mods.Value.ToArray()).ToArray(); + if (player != null) + { + gameplayRuleset = player.GameplayRuleset; + timedAttributes = gameplayRuleset.CreateDifficultyCalculator(new GameplayWorkingBeatmap(player.GameplayBeatmap)).CalculateTimed(player.Mods.Value.ToArray()).ToArray(); + } } protected override void LoadComplete() { base.LoadComplete(); - scoreProcessor.NewJudgement += onNewJudgement; + + if (scoreProcessor != null) + scoreProcessor.NewJudgement += onNewJudgement; } private void onNewJudgement(JudgementResult judgement) { + if (player == null) + return; + var attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new DifficultyCalculator.TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); if (attribIndex < 0) attribIndex = ~attribIndex - 1; From fab0d531bec9bf64687e927b937f27e861b4b7ea Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Sep 2021 17:55:00 +0900 Subject: [PATCH 097/391] Add counter to HUD --- osu.Game/Skinning/DefaultSkin.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index d3adae5c8c..41b7875cd1 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -68,6 +68,7 @@ namespace osu.Game.Skinning var score = container.OfType().FirstOrDefault(); var accuracy = container.OfType().FirstOrDefault(); var combo = container.OfType().FirstOrDefault(); + var ppCounter = container.OfType().FirstOrDefault(); if (score != null) { @@ -81,6 +82,13 @@ namespace osu.Game.Skinning score.Position = new Vector2(0, vertical_offset); + if (ppCounter != null) + { + ppCounter.Y = score.Position.Y + score.ScreenSpaceDrawQuad.Size.Y; + ppCounter.Origin = Anchor.TopCentre; + ppCounter.Anchor = Anchor.TopCentre; + } + if (accuracy != null) { accuracy.Position = new Vector2(-accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 - horizontal_padding, vertical_offset + 5); @@ -123,6 +131,7 @@ namespace osu.Game.Skinning new SongProgress(), new BarHitErrorMeter(), new BarHitErrorMeter(), + new DefaultPerformancePointsCounter() } }; From fd13142a158b50f89caab9a516617137970e3388 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 18:20:20 +0900 Subject: [PATCH 098/391] Add missing interface to `BeatmapManager` --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 6ffdfa24b5..c72d1e8dec 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps /// Handles general operations related to global beatmap management. /// [ExcludeFromDynamicCompile] - public class BeatmapManager : IModelDownloader, IModelFileManager, ICanAcceptFiles, IWorkingBeatmapCache + public class BeatmapManager : IModelDownloader, IModelManager, IModelFileManager, ICanAcceptFiles, IWorkingBeatmapCache { private readonly BeatmapModelManager beatmapModelManager; private readonly WorkingBeatmapCache workingBeatmapCache; From 0a00bc779542a3ce86c77a9970e14c8cfab06f34 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 17:42:12 +0900 Subject: [PATCH 099/391] Split out `IPostNotifications` into an interface --- osu.Game/Collections/CollectionManager.cs | 6 ++---- osu.Game/Database/ArchiveModelManager.cs | 3 --- osu.Game/Database/IModelManager.cs | 2 +- osu.Game/Database/IPostNotifications.cs | 16 ++++++++++++++++ 4 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Database/IPostNotifications.cs diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index fe04c70d62..6f9d9cd8a8 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.IO; using osu.Game.IO.Legacy; using osu.Game.Overlays.Notifications; @@ -27,7 +28,7 @@ namespace osu.Game.Collections /// This is currently reading and writing from the osu-stable file format. This is a temporary arrangement until we refactor the /// database backing the game. Going forward writing should be done in a similar way to other model stores. /// - public class CollectionManager : Component + public class CollectionManager : Component, IPostNotifications { /// /// Database version in stable-compatible YYYYMMDD format. @@ -106,9 +107,6 @@ namespace osu.Game.Collections backgroundSave(); }); - /// - /// Set an endpoint for notifications to be posted to. - /// public Action PostNotification { protected get; set; } /// diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index fc217d3058..018b41ebc1 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -57,9 +57,6 @@ namespace osu.Game.Database /// private static readonly ThreadedTaskScheduler import_scheduler_low_priority = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelManager)); - /// - /// Set an endpoint for notifications to be posted to. - /// public Action PostNotification { protected get; set; } /// diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index 8f0c6e1561..721de8f3a0 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.cs @@ -17,7 +17,7 @@ namespace osu.Game.Database /// Represents a model manager that publishes events when s are added or removed. /// /// The model type. - public interface IModelManager + public interface IModelManager : IPostNotifications where TModel : class { /// diff --git a/osu.Game/Database/IPostNotifications.cs b/osu.Game/Database/IPostNotifications.cs new file mode 100644 index 0000000000..d4fd64e79e --- /dev/null +++ b/osu.Game/Database/IPostNotifications.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 osu.Game.Overlays.Notifications; + +namespace osu.Game.Database +{ + public interface IPostNotifications + { + /// + /// And action which will be fired when a notification should be presented to the user. + /// + public Action PostNotification { set; } + } +} From 3e3b9bc963583fa5f6ad3b822ad3e6e3818aa06c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 18:21:16 +0900 Subject: [PATCH 100/391] Split out `IModelDownloader` and also split apart `ScoreManager` --- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 25 +- osu.Game/Beatmaps/BeatmapManager.cs | 37 ++- osu.Game/Beatmaps/BeatmapModelDownloader.cs | 21 ++ osu.Game/Beatmaps/BeatmapModelManager.cs | 21 +- osu.Game/Database/ArchiveModelManager.cs | 20 +- osu.Game/Database/IModelDownloader.cs | 9 +- osu.Game/Database/IModelManager.cs | 12 + osu.Game/Database/IPresentImports.cs | 17 ++ ...hiveModelManager.cs => ModelDownloader.cs} | 47 ++-- osu.Game/Online/DownloadTrackingComposite.cs | 10 +- osu.Game/Scoring/ScoreManager.cs | 222 ++++++++++++------ osu.Game/Scoring/ScoreModelDownloader.cs | 20 ++ osu.Game/Scoring/ScoreModelManager.cs | 88 +++++++ .../Screens/Ranking/ReplayDownloadButton.cs | 2 +- osu.Game/Tests/Visual/EditorTestScene.cs | 2 +- 15 files changed, 403 insertions(+), 150 deletions(-) create mode 100644 osu.Game/Beatmaps/BeatmapModelDownloader.cs create mode 100644 osu.Game/Database/IPresentImports.cs rename osu.Game/Database/{DownloadableArchiveModelManager.cs => ModelDownloader.cs} (68%) create mode 100644 osu.Game/Scoring/ScoreModelDownloader.cs create mode 100644 osu.Game/Scoring/ScoreModelManager.cs diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 1a3f9e414d..d38294aba9 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -158,14 +158,30 @@ namespace osu.Game.Tests.Online public Task CurrentImportTask { get; private set; } + public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) + : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap) + { + } + protected override BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) { return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, api, host); } - public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) - : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap) + protected override BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host) { + return new TestBeatmapModelDownloader(modelManager, api, host); + } + + internal class TestBeatmapModelDownloader : BeatmapModelDownloader + { + public TestBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost) + : base(modelManager, apiProvider, gameHost) + { + } + + protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) + => new TestDownloadRequest(set); } internal class TestBeatmapModelManager : BeatmapModelManager @@ -173,14 +189,11 @@ namespace osu.Game.Tests.Online private readonly TestBeatmapManager testBeatmapManager; public TestBeatmapModelManager(TestBeatmapManager testBeatmapManager, Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost) - : base(storage, databaseContextFactory, rulesetStore, apiProvider, gameHost) + : base(storage, databaseContextFactory, rulesetStore, gameHost) { this.testBeatmapManager = testBeatmapManager; } - protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) - => new TestDownloadRequest(set); - public override async Task Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { await testBeatmapManager.AllowImport.Task.ConfigureAwait(false); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index c72d1e8dec..8dfd895987 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -32,12 +32,15 @@ namespace osu.Game.Beatmaps public class BeatmapManager : IModelDownloader, IModelManager, IModelFileManager, ICanAcceptFiles, IWorkingBeatmapCache { private readonly BeatmapModelManager beatmapModelManager; + private readonly BeatmapModelDownloader beatmapModelDownloader; + private readonly WorkingBeatmapCache workingBeatmapCache; public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) { beatmapModelManager = CreateBeatmapModelManager(storage, contextFactory, rulesets, api, host); + beatmapModelDownloader = CreateBeatmapModelDownloader(beatmapModelManager, api, host); workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, resources, new FileStore(contextFactory, storage).Store, defaultBeatmap, host); workingBeatmapCache.BeatmapManager = beatmapModelManager; @@ -49,11 +52,16 @@ namespace osu.Game.Beatmaps } } + protected virtual BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host) + { + return new BeatmapModelDownloader(modelManager, api, host); + } + protected virtual WorkingBeatmapCache CreateWorkingBeatmapCache(AudioManager audioManager, IResourceStore resources, IResourceStore storage, WorkingBeatmap defaultBeatmap, GameHost host) => new WorkingBeatmapCache(audioManager, resources, storage, defaultBeatmap, host); protected virtual BeatmapModelManager CreateBeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host) => - new BeatmapModelManager(storage, contextFactory, rulesets, api, host); + new BeatmapModelManager(storage, contextFactory, rulesets, host); /// /// Create a new . @@ -156,7 +164,14 @@ namespace osu.Game.Beatmaps /// /// Fired when a notification should be presented to the user. /// - public Action PostNotification { set => beatmapModelManager.PostNotification = value; } + public Action PostNotification + { + set + { + beatmapModelManager.PostNotification = value; + beatmapModelDownloader.PostNotification = value; + } + } /// /// Fired when the user requests to view the resulting import. @@ -179,6 +194,11 @@ namespace osu.Game.Beatmaps #region Implementation of IModelManager + public bool IsAvailableLocally(BeatmapSetInfo model) + { + return beatmapModelManager.IsAvailableLocally(model); + } + public IBindable> ItemUpdated => beatmapModelManager.ItemUpdated; public IBindable> ItemRemoved => beatmapModelManager.ItemRemoved; @@ -227,23 +247,18 @@ namespace osu.Game.Beatmaps #region Implementation of IModelDownloader - public IBindable>> DownloadBegan => beatmapModelManager.DownloadBegan; + public IBindable>> DownloadBegan => beatmapModelDownloader.DownloadBegan; - public IBindable>> DownloadFailed => beatmapModelManager.DownloadFailed; - - public bool IsAvailableLocally(BeatmapSetInfo model) - { - return beatmapModelManager.IsAvailableLocally(model); - } + public IBindable>> DownloadFailed => beatmapModelDownloader.DownloadFailed; public bool Download(BeatmapSetInfo model, bool minimiseDownloadSize = false) { - return beatmapModelManager.Download(model, minimiseDownloadSize); + return beatmapModelDownloader.Download(model, minimiseDownloadSize); } public ArchiveDownloadRequest GetExistingDownload(BeatmapSetInfo model) { - return beatmapModelManager.GetExistingDownload(model); + return beatmapModelDownloader.GetExistingDownload(model); } #endregion diff --git a/osu.Game/Beatmaps/BeatmapModelDownloader.cs b/osu.Game/Beatmaps/BeatmapModelDownloader.cs new file mode 100644 index 0000000000..ae482eeafd --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapModelDownloader.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.Platform; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; + +namespace osu.Game.Beatmaps +{ + public class BeatmapModelDownloader : ModelDownloader + { + protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => + new DownloadBeatmapSetRequest(set, minimiseDownloadSize); + + public BeatmapModelDownloader(BeatmapModelManager beatmapModelManager, IAPIProvider api, GameHost host = null) + : base(beatmapModelManager, api, host) + { + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index be3adc412c..1b6694b1b4 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -21,8 +21,6 @@ using osu.Game.Beatmaps.Formats; using osu.Game.Database; using osu.Game.IO; using osu.Game.IO.Archives; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Skinning; @@ -34,7 +32,7 @@ namespace osu.Game.Beatmaps /// Handles ef-core storage of beatmaps. /// [ExcludeFromDynamicCompile] - public class BeatmapModelManager : DownloadableArchiveModelManager + public class BeatmapModelManager : ArchiveModelManager { /// /// Fired when a single difficulty has been hidden. @@ -72,8 +70,8 @@ namespace osu.Game.Beatmaps private readonly BeatmapStore beatmaps; private readonly RulesetStore rulesets; - public BeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, GameHost host = null) - : base(storage, contextFactory, api, new BeatmapStore(contextFactory), host) + public BeatmapModelManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, GameHost host = null) + : base(storage, contextFactory, new BeatmapStore(contextFactory), host) { this.rulesets = rulesets; @@ -84,9 +82,6 @@ namespace osu.Game.Beatmaps beatmaps.ItemUpdated += obj => WorkingBeatmapCache?.Invalidate(obj); } - protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => - new DownloadBeatmapSetRequest(set, minimiseDownloadSize); - protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) @@ -176,10 +171,6 @@ namespace osu.Game.Beatmaps void resetIds() => beatmapSet.Beatmaps.ForEach(b => b.OnlineBeatmapID = null); } - protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable items) - => base.CheckLocalAvailability(model, items) - || (model.OnlineBeatmapSetID != null && items.Any(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID)); - /// /// Delete a beatmap difficulty. /// @@ -347,7 +338,11 @@ namespace osu.Game.Beatmaps /// Results from the provided query. public IQueryable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); - protected override string HumanisedModelName => "beatmap"; + public override string HumanisedModelName => "beatmap"; + + protected override bool CheckLocalAvailability(BeatmapSetInfo model, IQueryable items) + => base.CheckLocalAvailability(model, items) + || (model.OnlineBeatmapSetID != null && items.Any(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID)); protected override BeatmapSetInfo CreateModel(ArchiveReader reader) { diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 018b41ebc1..0c309bbddb 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -30,7 +30,7 @@ namespace osu.Game.Database /// /// The model type. /// The associated file join type. - public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager + public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager, IPresentImports where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete where TFileModel : class, INamedFileInfo, new() { @@ -249,10 +249,7 @@ namespace osu.Game.Database return import; } - /// - /// Fired when the user requests to view the resulting import. - /// - public Action> PresentImport; + public Action> PresentImport { protected get; set; } /// /// Silently import an item from an . @@ -799,6 +796,17 @@ namespace osu.Game.Database /// An existing model which matches the criteria to skip importing, else null. protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash); + public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, ModelStore.ConsumableItems.Where(m => !m.DeletePending)); + + /// + /// Performs implementation specific comparisons to determine whether a given model is present in the local store. + /// + /// The whose existence needs to be checked. + /// The usable items present in the store. + /// Whether the exists. + protected virtual bool CheckLocalAvailability(TModel model, IQueryable items) + => model.ID > 0 && items.Any(i => i.ID == model.ID && i.Files.Any()); + /// /// Whether import can be skipped after finding an existing import early in the process. /// Only valid when is not overridden. @@ -835,7 +843,7 @@ namespace osu.Game.Database private DbSet queryModel() => ContextFactory.Get().Set(); - protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; + public virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; #region Event handling / delaying diff --git a/osu.Game/Database/IModelDownloader.cs b/osu.Game/Database/IModelDownloader.cs index 0cb633280e..a5573b2190 100644 --- a/osu.Game/Database/IModelDownloader.cs +++ b/osu.Game/Database/IModelDownloader.cs @@ -11,7 +11,7 @@ namespace osu.Game.Database /// Represents a that can download new models from an external source. /// /// The model type. - public interface IModelDownloader : IModelManager + public interface IModelDownloader : IPostNotifications where TModel : class { /// @@ -26,13 +26,6 @@ namespace osu.Game.Database /// IBindable>> DownloadFailed { get; } - /// - /// Checks whether a given is already available in the local store. - /// - /// The whose existence needs to be checked. - /// Whether the exists. - bool IsAvailableLocally(TModel model); - /// /// Begin a download for the requested . /// diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index 721de8f3a0..7bfc8dbee3 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.cs @@ -123,5 +123,17 @@ namespace osu.Game.Database /// Whether this is a low priority import. /// An optional cancellation token. Task Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); + + /// + /// Checks whether a given is already available in the local store. + /// + /// The whose existence needs to be checked. + /// Whether the exists. + bool IsAvailableLocally(TModel model); + + /// + /// A user displayable name for the model type associated with this manager. + /// + string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; } } diff --git a/osu.Game/Database/IPresentImports.cs b/osu.Game/Database/IPresentImports.cs new file mode 100644 index 0000000000..39b495ebd5 --- /dev/null +++ b/osu.Game/Database/IPresentImports.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 System; +using System.Collections.Generic; + +namespace osu.Game.Database +{ + public interface IPresentImports + where TModel : class + { + /// + /// Fired when the user requests to view the resulting import. + /// + public Action> PresentImport { set; } + } +} diff --git a/osu.Game/Database/DownloadableArchiveModelManager.cs b/osu.Game/Database/ModelDownloader.cs similarity index 68% rename from osu.Game/Database/DownloadableArchiveModelManager.cs rename to osu.Game/Database/ModelDownloader.cs index e6d5b44b65..e613b39b6b 100644 --- a/osu.Game/Database/DownloadableArchiveModelManager.cs +++ b/osu.Game/Database/ModelDownloader.cs @@ -1,29 +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 Humanizer; -using osu.Framework.Logging; -using osu.Framework.Platform; -using osu.Game.Online.API; -using osu.Game.Overlays.Notifications; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Humanizer; using osu.Framework.Bindables; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Online.API; +using osu.Game.Overlays.Notifications; namespace osu.Game.Database { - /// - /// An that has the ability to download models using an and - /// import them into the store. - /// - /// The model type. - /// The associated file join type. - public abstract class DownloadableArchiveModelManager : ArchiveModelManager, IModelDownloader - where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete, IEquatable - where TFileModel : class, INamedFileInfo, new() + public abstract class ModelDownloader : IModelDownloader + where TModel : class, IHasPrimaryKey, ISoftDelete, IEquatable { + public Action PostNotification { protected get; set; } + public IBindable>> DownloadBegan => downloadBegan; private readonly Bindable>> downloadBegan = new Bindable>>(); @@ -32,18 +27,15 @@ namespace osu.Game.Database private readonly Bindable>> downloadFailed = new Bindable>>(); + private readonly IModelManager modelManager; private readonly IAPIProvider api; private readonly List> currentDownloads = new List>(); - private readonly MutableDatabaseBackedStoreWithFileIncludes modelStore; - - protected DownloadableArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, IAPIProvider api, MutableDatabaseBackedStoreWithFileIncludes modelStore, - IIpcHost importHost = null) - : base(storage, contextFactory, modelStore, importHost) + protected ModelDownloader(IModelManager modelManager, IAPIProvider api, IIpcHost importHost = null) { + this.modelManager = modelManager; this.api = api; - this.modelStore = modelStore; } /// @@ -76,7 +68,7 @@ namespace osu.Game.Database Task.Factory.StartNew(async () => { // This gets scheduled back to the update thread, but we want the import to run in the background. - var imported = await Import(notification, new ImportTask(filename)).ConfigureAwait(false); + var imported = await modelManager.Import(notification, new ImportTask(filename)).ConfigureAwait(false); // for now a failed import will be marked as a failed download for simplicity. if (!imported.Any()) @@ -111,21 +103,10 @@ namespace osu.Game.Database notification.State = ProgressNotificationState.Cancelled; if (!(error is OperationCanceledException)) - Logger.Error(error, $"{HumanisedModelName.Titleize()} download failed!"); + Logger.Error(error, $"{modelManager.HumanisedModelName.Titleize()} download failed!"); } } - public bool IsAvailableLocally(TModel model) => CheckLocalAvailability(model, modelStore.ConsumableItems.Where(m => !m.DeletePending)); - - /// - /// Performs implementation specific comparisons to determine whether a given model is present in the local store. - /// - /// The whose existence needs to be checked. - /// The usable items present in the store. - /// Whether the exists. - protected virtual bool CheckLocalAvailability(TModel model, IQueryable items) - => model.ID > 0 && items.Any(i => i.ID == model.ID && i.Files.Any()); - public ArchiveDownloadRequest GetExistingDownload(TModel model) => currentDownloads.Find(r => r.Model.Equals(model)); private bool canDownload(TModel model) => GetExistingDownload(model) == null && api != null; diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index d9599481e7..2a96051427 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -16,7 +16,7 @@ namespace osu.Game.Online /// public abstract class DownloadTrackingComposite : CompositeDrawable where TModel : class, IEquatable - where TModelManager : class, IModelDownloader + where TModelManager : class, IModelDownloader, IModelManager { protected readonly Bindable Model = new Bindable(); @@ -35,7 +35,7 @@ namespace osu.Game.Online Model.Value = model; } - private IBindable> managedUpdated; + private IBindable> managerUpdated; private IBindable> managerRemoved; private IBindable>> managerDownloadBegan; private IBindable>> managerDownloadFailed; @@ -60,8 +60,8 @@ namespace osu.Game.Online managerDownloadBegan.BindValueChanged(downloadBegan); managerDownloadFailed = Manager.DownloadFailed.GetBoundCopy(); managerDownloadFailed.BindValueChanged(downloadFailed); - managedUpdated = Manager.ItemUpdated.GetBoundCopy(); - managedUpdated.BindValueChanged(itemUpdated); + managerUpdated = Manager.ItemUpdated.GetBoundCopy(); + managerUpdated.BindValueChanged(itemUpdated); managerRemoved = Manager.ItemRemoved.GetBoundCopy(); managerRemoved.BindValueChanged(itemRemoved); } @@ -77,7 +77,7 @@ namespace osu.Game.Online /// /// Whether the given model is available in the database. - /// By default, this calls , + /// By default, this calls , /// but can be overriden to add additional checks for verifying the model in database. /// protected virtual bool IsModelAvailableLocally() => Manager?.IsAvailableLocally(Model.Value) == true; diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 56c346d177..d83b4e3f1d 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -9,102 +9,48 @@ using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; using osu.Framework.Bindables; -using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; +using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring.Legacy; namespace osu.Game.Scoring { - public class ScoreManager : DownloadableArchiveModelManager + public class ScoreManager : IModelManager, IModelFileManager, IModelDownloader, ICanAcceptFiles, IPresentImports { - public override IEnumerable HandledExtensions => new[] { ".osr" }; - - protected override string[] HashableFileTypes => new[] { ".osr" }; - - protected override string ImportFromStablePath => Path.Combine("Data", "r"); - - private readonly RulesetStore rulesets; - private readonly Func beatmaps; private readonly Scheduler scheduler; - - [CanBeNull] private readonly Func difficulties; - - [CanBeNull] private readonly OsuConfigManager configManager; + private readonly ScoreModelManager scoreModelManager; + private readonly ScoreModelDownloader scoreModelDownloader; public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, Scheduler scheduler, IIpcHost importHost = null, Func difficulties = null, OsuConfigManager configManager = null) - : base(storage, contextFactory, api, new ScoreStore(contextFactory, storage), importHost) { - this.rulesets = rulesets; - this.beatmaps = beatmaps; this.scheduler = scheduler; this.difficulties = difficulties; this.configManager = configManager; + + scoreModelManager = new ScoreModelManager(rulesets, beatmaps, storage, contextFactory, importHost); + scoreModelDownloader = new ScoreModelDownloader(scoreModelManager, api, importHost); } - protected override ScoreInfo CreateModel(ArchiveReader archive) - { - if (archive == null) - return null; + public Score GetScore(ScoreInfo score) => scoreModelManager.GetScore(score); - using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase)))) - { - try - { - return new DatabasedLegacyScoreDecoder(rulesets, beatmaps()).Parse(stream).ScoreInfo; - } - catch (LegacyScoreDecoder.BeatmapNotFoundException e) - { - Logger.Log(e.Message, LoggingTarget.Information, LogLevel.Error); - return null; - } - } - } + public List GetAllUsableScores() => scoreModelManager.GetAllUsableScores(); - protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) - => Task.CompletedTask; + public IEnumerable QueryScores(Expression> query) => scoreModelManager.QueryScores(query); - public override void ExportModelTo(ScoreInfo model, Stream outputStream) - { - var file = model.Files.SingleOrDefault(); - if (file == null) - return; - - using (var inputStream = Files.Storage.GetStream(file.FileInfo.StoragePath)) - inputStream.CopyTo(outputStream); - } - - protected override IEnumerable GetStableImportPaths(Storage storage) - => storage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)) - .Select(path => storage.GetFullPath(path)); - - public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); - - public List GetAllUsableScores() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); - - public IEnumerable QueryScores(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().Where(query); - - public ScoreInfo Query(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query); - - protected override ArchiveDownloadRequest CreateDownloadRequest(ScoreInfo score, bool minimiseDownload) => new DownloadReplayRequest(score); - - protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable items) - => base.CheckLocalAvailability(model, items) - || (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID)); + public ScoreInfo Query(Expression> query) => scoreModelManager.Query(query); /// /// Orders an array of s by total score. @@ -281,5 +227,149 @@ namespace osu.Game.Scoring this.totalScore.BindValueChanged(v => Value = v.NewValue.ToString("N0"), true); } } + + #region Implementation of IPostNotifications + + public Action PostNotification + { + set + { + scoreModelManager.PostNotification = value; + scoreModelDownloader.PostNotification = value; + } + } + + #endregion + + #region Implementation of IModelManager + + public IBindable> ItemUpdated => scoreModelManager.ItemUpdated; + + public IBindable> ItemRemoved => scoreModelManager.ItemRemoved; + + public Task ImportFromStableAsync(StableStorage stableStorage) + { + return scoreModelManager.ImportFromStableAsync(stableStorage); + } + + public void Export(ScoreInfo item) + { + scoreModelManager.Export(item); + } + + public void ExportModelTo(ScoreInfo model, Stream outputStream) + { + scoreModelManager.ExportModelTo(model, outputStream); + } + + public void Update(ScoreInfo item) + { + scoreModelManager.Update(item); + } + + public bool Delete(ScoreInfo item) + { + return scoreModelManager.Delete(item); + } + + public void Delete(List items, bool silent = false) + { + scoreModelManager.Delete(items, silent); + } + + public void Undelete(List items, bool silent = false) + { + scoreModelManager.Undelete(items, silent); + } + + public void Undelete(ScoreInfo item) + { + scoreModelManager.Undelete(item); + } + + public Task Import(params string[] paths) + { + return scoreModelManager.Import(paths); + } + + public Task Import(params ImportTask[] tasks) + { + return scoreModelManager.Import(tasks); + } + + public IEnumerable HandledExtensions => scoreModelManager.HandledExtensions; + + public Task> Import(ProgressNotification notification, params ImportTask[] tasks) + { + return scoreModelManager.Import(notification, tasks); + } + + public Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return scoreModelManager.Import(task, lowPriority, cancellationToken); + } + + public Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return scoreModelManager.Import(archive, lowPriority, cancellationToken); + } + + public Task Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + { + return scoreModelManager.Import(item, archive, lowPriority, cancellationToken); + } + + public bool IsAvailableLocally(ScoreInfo model) + { + return scoreModelManager.IsAvailableLocally(model); + } + + #endregion + + #region Implementation of IModelFileManager + + public void ReplaceFile(ScoreInfo model, ScoreFileInfo file, Stream contents, string filename = null) + { + scoreModelManager.ReplaceFile(model, file, contents, filename); + } + + public void DeleteFile(ScoreInfo model, ScoreFileInfo file) + { + scoreModelManager.DeleteFile(model, file); + } + + public void AddFile(ScoreInfo model, Stream contents, string filename) + { + scoreModelManager.AddFile(model, contents, filename); + } + + #endregion + + #region Implementation of IModelDownloader + + public IBindable>> DownloadBegan => scoreModelDownloader.DownloadBegan; + + public IBindable>> DownloadFailed => scoreModelDownloader.DownloadFailed; + + public bool Download(ScoreInfo model, bool minimiseDownloadSize) + { + return scoreModelDownloader.Download(model, minimiseDownloadSize); + } + + public ArchiveDownloadRequest GetExistingDownload(ScoreInfo model) + { + return scoreModelDownloader.GetExistingDownload(model); + } + + #endregion + + #region Implementation of IPresentImports + + public Action> PresentImport + { + set => scoreModelManager.PresentImport = value; + } + + #endregion } } diff --git a/osu.Game/Scoring/ScoreModelDownloader.cs b/osu.Game/Scoring/ScoreModelDownloader.cs new file mode 100644 index 0000000000..b3c1e2928a --- /dev/null +++ b/osu.Game/Scoring/ScoreModelDownloader.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.Platform; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; + +namespace osu.Game.Scoring +{ + public class ScoreModelDownloader : ModelDownloader + { + public ScoreModelDownloader(ScoreModelManager scoreManager, IAPIProvider api, IIpcHost importHost = null) + : base(scoreManager, api, importHost) + { + } + + protected override ArchiveDownloadRequest CreateDownloadRequest(ScoreInfo score, bool minimiseDownload) => new DownloadReplayRequest(score); + } +} diff --git a/osu.Game/Scoring/ScoreModelManager.cs b/osu.Game/Scoring/ScoreModelManager.cs new file mode 100644 index 0000000000..c65a6acdfb --- /dev/null +++ b/osu.Game/Scoring/ScoreModelManager.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 System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.IO.Archives; +using osu.Game.Rulesets; +using osu.Game.Scoring.Legacy; + +namespace osu.Game.Scoring +{ + public class ScoreModelManager : ArchiveModelManager + { + public override IEnumerable HandledExtensions => new[] { ".osr" }; + + protected override string[] HashableFileTypes => new[] { ".osr" }; + + protected override string ImportFromStablePath => Path.Combine("Data", "r"); + + private readonly RulesetStore rulesets; + private readonly Func beatmaps; + + public ScoreModelManager(RulesetStore rulesets, Func beatmaps, Storage storage, IDatabaseContextFactory contextFactory, IIpcHost importHost = null) + : base(storage, contextFactory, new ScoreStore(contextFactory, storage), importHost) + { + this.rulesets = rulesets; + this.beatmaps = beatmaps; + } + + protected override ScoreInfo CreateModel(ArchiveReader archive) + { + if (archive == null) + return null; + + using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase)))) + { + try + { + return new DatabasedLegacyScoreDecoder(rulesets, beatmaps()).Parse(stream).ScoreInfo; + } + catch (LegacyScoreDecoder.BeatmapNotFoundException e) + { + Logger.Log(e.Message, LoggingTarget.Information, LogLevel.Error); + return null; + } + } + } + + public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); + + public List GetAllUsableScores() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); + + public IEnumerable QueryScores(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().Where(query); + + public ScoreInfo Query(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query); + + protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) + => Task.CompletedTask; + + protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable items) + => base.CheckLocalAvailability(model, items) + || (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID)); + + public override void ExportModelTo(ScoreInfo model, Stream outputStream) + { + var file = model.Files.SingleOrDefault(); + if (file == null) + return; + + using (var inputStream = Files.Storage.GetStream(file.FileInfo.StoragePath)) + inputStream.CopyTo(outputStream); + } + + protected override IEnumerable GetStableImportPaths(Storage storage) + => storage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)) + .Select(path => storage.GetFullPath(path)); + } +} diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index 18b8649a59..d96b6989b4 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Ranking } [BackgroundDependencyLoader(true)] - private void load(OsuGame game, ScoreManager scores) + private void load(OsuGame game, ScoreModelDownloader scores) { InternalChild = shakeContainer = new ShakeContainer { diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index ac8773a840..798b0d01ee 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -150,7 +150,7 @@ namespace osu.Game.Tests.Visual internal class TestBeatmapModelManager : BeatmapModelManager { public TestBeatmapModelManager(Storage storage, IDatabaseContextFactory databaseContextFactory, RulesetStore rulesetStore, IAPIProvider apiProvider, GameHost gameHost) - : base(storage, databaseContextFactory, rulesetStore, apiProvider, gameHost) + : base(storage, databaseContextFactory, rulesetStore, gameHost) { } From c05a8fc4a2ec1f598a6e317974d9293c34d780b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 18:52:09 +0900 Subject: [PATCH 101/391] Split importer interface out of `IModelManager` --- osu.Game/Database/IModelImporter.cs | 65 +++++++++++++++++++++++++++++ osu.Game/Database/IModelManager.cs | 51 +--------------------- 2 files changed, 66 insertions(+), 50 deletions(-) create mode 100644 osu.Game/Database/IModelImporter.cs diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs new file mode 100644 index 0000000000..fa3b4d9152 --- /dev/null +++ b/osu.Game/Database/IModelImporter.cs @@ -0,0 +1,65 @@ +// 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.Threading; +using System.Threading.Tasks; +using osu.Game.IO.Archives; +using osu.Game.Overlays.Notifications; + +namespace osu.Game.Database +{ + /// + /// A class which handles importing of asociated models to the game store. + /// + /// The model type. + public interface IModelImporter : IPostNotifications + where TModel : class + { + /// + /// Import one or more items from filesystem . + /// + /// + /// This will be treated as a low priority import if more than one path is specified; use to always import at standard priority. + /// This will post notifications tracking progress. + /// + /// One or more archive locations on disk. + Task Import(params string[] paths); + + Task Import(params ImportTask[] tasks); + + Task> Import(ProgressNotification notification, params ImportTask[] tasks); + + /// + /// 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. + /// + /// The containing data about the to import. + /// Whether this is a low priority import. + /// An optional cancellation token. + /// The imported model, if successful. + Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default); + + /// + /// Silently import an item from an . + /// + /// The archive to be imported. + /// Whether this is a low priority import. + /// An optional cancellation token. + Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default); + + /// + /// Silently import an item from a . + /// + /// The model to be imported. + /// An optional archive to use for model population. + /// Whether this is a low priority import. + /// An optional cancellation token. + Task Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); + + /// + /// A user displayable name for the model type associated with this manager. + /// + string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; + } +} diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index 7bfc8dbee3..2b1e574176 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.cs @@ -4,12 +4,9 @@ using System; using System.Collections.Generic; using System.IO; -using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; using osu.Game.IO; -using osu.Game.IO.Archives; -using osu.Game.Overlays.Notifications; namespace osu.Game.Database { @@ -17,7 +14,7 @@ namespace osu.Game.Database /// Represents a model manager that publishes events when s are added or removed. /// /// The model type. - public interface IModelManager : IPostNotifications + public interface IModelManager : IModelImporter, IPostNotifications where TModel : class { /// @@ -83,57 +80,11 @@ namespace osu.Game.Database /// The item to restore void Undelete(TModel item); - /// - /// Import one or more items from filesystem . - /// - /// - /// This will be treated as a low priority import if more than one path is specified; use to always import at standard priority. - /// This will post notifications tracking progress. - /// - /// One or more archive locations on disk. - Task Import(params string[] paths); - - Task Import(params ImportTask[] tasks); - - Task> Import(ProgressNotification notification, params ImportTask[] tasks); - - /// - /// 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. - /// - /// The containing data about the to import. - /// Whether this is a low priority import. - /// An optional cancellation token. - /// The imported model, if successful. - Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default); - - /// - /// Silently import an item from an . - /// - /// The archive to be imported. - /// Whether this is a low priority import. - /// An optional cancellation token. - Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default); - - /// - /// Silently import an item from a . - /// - /// The model to be imported. - /// An optional archive to use for model population. - /// Whether this is a low priority import. - /// An optional cancellation token. - Task Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); - /// /// Checks whether a given is already available in the local store. /// /// The whose existence needs to be checked. /// Whether the exists. bool IsAvailableLocally(TModel model); - - /// - /// A user displayable name for the model type associated with this manager. - /// - string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}"; } } From 66409147dc3ff3cc3eac9afa10841ef35e6eef98 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 19:25:08 +0900 Subject: [PATCH 102/391] Remove duplicate interface specification --- osu.Game/Database/IModelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs index 2b1e574176..f5e401cdfb 100644 --- a/osu.Game/Database/IModelManager.cs +++ b/osu.Game/Database/IModelManager.cs @@ -14,7 +14,7 @@ namespace osu.Game.Database /// Represents a model manager that publishes events when s are added or removed. /// /// The model type. - public interface IModelManager : IModelImporter, IPostNotifications + public interface IModelManager : IModelImporter where TModel : class { /// From a2e61883e3b00dc205ecefc5870d9d2343ced7bd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 19:33:12 +0900 Subject: [PATCH 103/391] Initial push to use `ILive` in import process --- .../Beatmaps/IO/ImportBeatmapTest.cs | 34 +++++++-------- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 4 +- osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 2 +- .../Skins/TestSceneBeatmapSkinResources.cs | 2 +- .../Skins/TestSceneSkinResources.cs | 2 +- .../Menus/TestSceneMusicActionHandling.cs | 2 +- .../Navigation/TestScenePresentBeatmap.cs | 2 +- .../Navigation/TestScenePresentScore.cs | 4 +- .../TestScenePlaylistsRoomSubScreen.cs | 2 +- .../TestSceneBeatmapRecommendations.cs | 2 +- .../SongSelect/TestScenePlaySongSelect.cs | 2 +- .../TestSceneDeleteLocalScore.cs | 4 +- osu.Game/Beatmaps/BeatmapManager.cs | 15 +++---- osu.Game/Database/ArchiveModelManager.cs | 26 ++++++------ osu.Game/Database/EntityFrameworkLive.cs | 34 +++++++++++++++ .../Database/EntityFrameworkLiveExtensions.cs | 14 +++++++ osu.Game/Database/ILive.cs | 42 +++++++++++++++++++ osu.Game/Database/IModelImporter.cs | 8 ++-- osu.Game/Database/IPresentImports.cs | 2 +- osu.Game/FodyWeavers.xml | 3 ++ osu.Game/OsuGame.cs | 4 +- osu.Game/Scoring/ScoreManager.cs | 10 ++--- osu.Game/Screens/Menu/IntroScreen.cs | 8 +++- osu.Game/Skinning/SkinManager.cs | 2 +- 24 files changed, 164 insertions(+), 66 deletions(-) create mode 100644 osu.Game/Database/EntityFrameworkLive.cs create mode 100644 osu.Game/Database/EntityFrameworkLiveExtensions.cs create mode 100644 osu.Game/Database/ILive.cs create mode 100644 osu.Game/FodyWeavers.xml diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index cba7f34ede..fc2a3792cb 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -86,7 +86,7 @@ namespace osu.Game.Tests.Beatmaps.IO var manager = osu.Dependencies.Get(); - BeatmapSetInfo importedSet; + ILive importedSet; using (var stream = File.OpenRead(tempPath)) { @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing"); File.Delete(tempPath); - var imported = manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); + var imported = manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID); deleteBeatmapSet(imported, osu); } @@ -172,8 +172,8 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); // but contents doesn't, so existing should still be used. - Assert.IsTrue(imported.ID == importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); + Assert.IsTrue(imported.ID == importedSecondTime.Value.ID); + Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Value.Beatmaps.First().ID); } finally { @@ -226,8 +226,8 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); // check the newly "imported" beatmap is not the original. - Assert.IsTrue(imported.ID != importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); + Assert.IsTrue(imported.ID != importedSecondTime.Value.ID); + Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID); } finally { @@ -278,8 +278,8 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); // check the newly "imported" beatmap is not the original. - Assert.IsTrue(imported.ID != importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); + Assert.IsTrue(imported.ID != importedSecondTime.Value.ID); + Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID); } finally { @@ -329,8 +329,8 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); // check the newly "imported" beatmap is not the original. - Assert.IsTrue(imported.ID != importedSecondTime.ID); - Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID); + Assert.IsTrue(imported.ID != importedSecondTime.Value.ID); + Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Value.Beatmaps.First().ID); } finally { @@ -570,8 +570,8 @@ namespace osu.Game.Tests.Beatmaps.IO var imported = await manager.Import(toImport); Assert.NotNull(imported); - Assert.AreEqual(null, imported.Beatmaps[0].OnlineBeatmapID); - Assert.AreEqual(null, imported.Beatmaps[1].OnlineBeatmapID); + Assert.AreEqual(null, imported.Value.Beatmaps[0].OnlineBeatmapID); + Assert.AreEqual(null, imported.Value.Beatmaps[1].OnlineBeatmapID); } finally { @@ -706,7 +706,7 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); - Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("subfolder")), "Files contain common subfolder"); + Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("subfolder")), "Files contain common subfolder"); } finally { @@ -759,8 +759,8 @@ namespace osu.Game.Tests.Beatmaps.IO ensureLoaded(osu); - Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("__MACOSX")), "Files contain resource fork folder, which should be ignored"); - Assert.IsFalse(imported.Files.Any(f => f.Filename.Contains("actual_data")), "Files contain common subfolder"); + Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("__MACOSX")), "Files contain resource fork folder, which should be ignored"); + Assert.IsFalse(imported.Value.Files.Any(f => f.Filename.Contains("actual_data")), "Files contain common subfolder"); } finally { @@ -915,7 +915,7 @@ namespace osu.Game.Tests.Beatmaps.IO waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); - return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); + return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID); } public static async Task LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false) @@ -930,7 +930,7 @@ namespace osu.Game.Tests.Beatmaps.IO waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); - return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); + return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.Value.ID); } private void deleteBeatmapSet(BeatmapSetInfo imported, OsuGameBase osu) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index d38294aba9..79767bc671 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -156,7 +156,7 @@ namespace osu.Game.Tests.Online { public TaskCompletionSource AllowImport = new TaskCompletionSource(); - public Task CurrentImportTask { get; private set; } + public Task> CurrentImportTask { get; private set; } public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null) : base(storage, contextFactory, rulesets, api, audioManager, resources, host, defaultBeatmap) @@ -194,7 +194,7 @@ namespace osu.Game.Tests.Online this.testBeatmapManager = testBeatmapManager; } - public override async Task Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public override async Task> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { await testBeatmapManager.AllowImport.Task.ConfigureAwait(false); return await (testBeatmapManager.CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)).ConfigureAwait(false); diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index 7a9fc20426..b2600bb887 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -196,7 +196,7 @@ namespace osu.Game.Tests.Skins.IO private async Task loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null) { var skinManager = osu.Dependencies.Get(); - return await skinManager.Import(archive); + return (await skinManager.Import(archive)).Value; } } } diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs index eff430ac25..f03cda1489 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Skins private void load() { var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).Result; - beatmap = beatmaps.GetWorkingBeatmap(imported.Beatmaps[0]); + beatmap = beatmaps.GetWorkingBeatmap(imported.Value.Beatmaps[0]); beatmap.LoadTrack(); } diff --git a/osu.Game.Tests/Skins/TestSceneSkinResources.cs b/osu.Game.Tests/Skins/TestSceneSkinResources.cs index 107a96292f..10f1ab31df 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinResources.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Skins private void load() { var imported = skins.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-skin.osk"))).Result; - skin = skins.GetSkin(imported); + skin = skins.GetSkin(imported.Value); } [Test] diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs index 9037338e23..79dfe79299 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("import beatmap with track", () => { var setWithTrack = Game.BeatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).Result; - Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(setWithTrack.Beatmaps.First()); + Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(setWithTrack.Value.Beatmaps.First()); }); AddStep("bind to track change", () => diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index f0ddefa51d..5f5ebfccfb 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -126,7 +126,7 @@ namespace osu.Game.Tests.Visual.Navigation Ruleset = ruleset ?? new OsuRuleset().RulesetInfo }, } - }).Result; + }).Result.Value; }); AddAssert($"import {i} succeeded", () => imported != null); diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index 52b577b402..2ea765a1a9 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Navigation Ruleset = new OsuRuleset().RulesetInfo }, } - }).Result; + }).Result.Value; }); } @@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Navigation OnlineScoreID = i, Beatmap = beatmap.Beatmaps.First(), Ruleset = ruleset ?? new OsuRuleset().RulesetInfo - }).Result; + }).Result.Value; }); AddAssert($"import {i} succeeded", () => imported != null); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index 9051c71fc6..d8ec89a94e 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Visual.Playlists { beatmap.BeatmapInfo.BaseDifficulty.CircleSize = 1; - importedSet = manager.Import(beatmap.BeatmapInfo.BeatmapSet).Result; + importedSet = manager.Import(beatmap.BeatmapInfo.BeatmapSet).Result.Value; }); AddStep("load room", () => diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 53cb628bb3..c22b6a54e9 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -192,7 +192,7 @@ namespace osu.Game.Tests.Visual.SongSelect }).ToList() }; - return Game.BeatmapManager.Import(beatmapSet).Result; + return Game.BeatmapManager.Import(beatmapSet).Result.Value; } private bool ensureAllBeatmapSetsImported(IEnumerable beatmapSets) => beatmapSets.All(set => set != null); diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 102e5ee425..19aa91a38f 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -751,7 +751,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("import huge difficulty count map", () => { var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); - imported = manager.Import(createTestBeatmapSet(usableRulesets, 50)).Result; + imported = manager.Import(createTestBeatmapSet(usableRulesets, 50)).Result.Value; }); AddStep("select the first beatmap of import", () => Beatmap.Value = manager.GetWorkingBeatmap(imported.Beatmaps.First())); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 2e30ed9827..3c69db032e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.UserInterface dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory, Scheduler)); - beatmap = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Beatmaps[0]; + beatmap = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Value.Beatmaps[0]; for (int i = 0; i < 50; i++) { @@ -100,7 +100,7 @@ namespace osu.Game.Tests.Visual.UserInterface User = new User { Username = "TestUser" }, }; - importedScores.Add(scoreManager.Import(score).Result); + importedScores.Add(scoreManager.Import(score).Result.Value); } return dependencies; diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 8dfd895987..1bf4feb6a3 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -89,8 +89,9 @@ namespace osu.Game.Beatmaps } }; - var working = beatmapModelManager.Import(set).Result; - return GetWorkingBeatmap(working.Beatmaps.First()); + var imported = beatmapModelManager.Import(set).Result.Value; + + return GetWorkingBeatmap(imported.Beatmaps.First()); } #region Delegation to BeatmapModelManager (methods which previously existed locally). @@ -176,7 +177,7 @@ namespace osu.Game.Beatmaps /// /// Fired when the user requests to view the resulting import. /// - public Action> PresentImport { set => beatmapModelManager.PresentImport = value; } + public Action>> PresentImport { set => beatmapModelManager.PresentImport = value; } /// /// Delete a beatmap difficulty. @@ -275,22 +276,22 @@ namespace osu.Game.Beatmaps return beatmapModelManager.Import(tasks); } - public Task> Import(ProgressNotification notification, params ImportTask[] tasks) + public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) { return beatmapModelManager.Import(notification, tasks); } - public Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) { return beatmapModelManager.Import(task, lowPriority, cancellationToken); } - public Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) { return beatmapModelManager.Import(archive, lowPriority, cancellationToken); } - public Task Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { return beatmapModelManager.Import(item, archive, lowPriority, cancellationToken); } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 0c309bbddb..403bfdf621 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -132,13 +132,13 @@ namespace osu.Game.Database return Import(notification, tasks); } - public async Task> Import(ProgressNotification notification, params ImportTask[] tasks) + public async Task>> Import(ProgressNotification notification, params ImportTask[] tasks) { if (tasks.Length == 0) { notification.CompletionText = $"No {HumanisedModelName}s were found to import!"; notification.State = ProgressNotificationState.Completed; - return Enumerable.Empty(); + return Enumerable.Empty>(); } notification.Progress = 0; @@ -146,7 +146,7 @@ namespace osu.Game.Database int current = 0; - var imported = new List(); + var imported = new List>(); bool isLowPriorityImport = tasks.Length > low_priority_import_batch_size; @@ -224,11 +224,11 @@ namespace osu.Game.Database /// Whether this is a low priority import. /// An optional cancellation token. /// The imported model, if successful. - public async Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + public async Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - TModel import; + ILive import; using (ArchiveReader reader = task.GetReader()) import = await Import(reader, lowPriority, cancellationToken).ConfigureAwait(false); @@ -243,13 +243,13 @@ namespace osu.Game.Database } catch (Exception e) { - LogForModel(import, $@"Could not delete original file after import ({task})", e); + LogForModel(import?.Value, $@"Could not delete original file after import ({task})", e); } return import; } - public Action> PresentImport { protected get; set; } + public Action>> PresentImport { protected get; set; } /// /// Silently import an item from an . @@ -257,7 +257,7 @@ namespace osu.Game.Database /// The archive to be imported. /// Whether this is a low priority import. /// An optional cancellation token. - public Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -268,7 +268,7 @@ namespace osu.Game.Database model = CreateModel(archive); if (model == null) - return Task.FromResult(null); + return Task.FromResult>(new EntityFrameworkLive(null)); } catch (TaskCanceledException) { @@ -343,7 +343,7 @@ namespace osu.Game.Database /// An optional archive to use for model population. /// Whether this is a low priority import. /// An optional cancellation token. - public virtual async Task Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) => await Task.Factory.StartNew(async () => + public virtual async Task> Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) => await Task.Factory.StartNew(async () => { cancellationToken.ThrowIfCancellationRequested(); @@ -369,7 +369,7 @@ namespace osu.Game.Database { LogForModel(item, @$"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import."); Undelete(existing); - return existing; + return existing.ToEntityFrameworkLive(); } LogForModel(item, @"Found existing (optimised) but failed pre-check."); @@ -415,7 +415,7 @@ namespace osu.Game.Database // existing item will be used; rollback new import and exit early. rollback(); flushEvents(true); - return existing; + return existing.ToEntityFrameworkLive(); } LogForModel(item, @"Found existing but failed re-use check."); @@ -448,7 +448,7 @@ namespace osu.Game.Database } flushEvents(true); - return item; + return item.ToEntityFrameworkLive(); }, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap().ConfigureAwait(false); /// diff --git a/osu.Game/Database/EntityFrameworkLive.cs b/osu.Game/Database/EntityFrameworkLive.cs new file mode 100644 index 0000000000..1d7b53911a --- /dev/null +++ b/osu.Game/Database/EntityFrameworkLive.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; + +namespace osu.Game.Database +{ + public class EntityFrameworkLive : ILive where T : class + { + public EntityFrameworkLive(T item) + { + Value = item; + } + + public Guid ID => throw new InvalidOperationException(); + + public void PerformRead(Action perform) + { + perform(Value); + } + + public TReturn PerformRead(Func perform) + { + return perform(Value); + } + + public void PerformWrite(Action perform) + { + perform(Value); + } + + public T Value { get; } + } +} diff --git a/osu.Game/Database/EntityFrameworkLiveExtensions.cs b/osu.Game/Database/EntityFrameworkLiveExtensions.cs new file mode 100644 index 0000000000..cd0673675e --- /dev/null +++ b/osu.Game/Database/EntityFrameworkLiveExtensions.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. + +namespace osu.Game.Database +{ + public static class EntityFrameworkLiveExtensions + { + public static ILive ToEntityFrameworkLive(this T item) + where T : class + { + return new EntityFrameworkLive(item); + } + } +} diff --git a/osu.Game/Database/ILive.cs b/osu.Game/Database/ILive.cs new file mode 100644 index 0000000000..29e5756dba --- /dev/null +++ b/osu.Game/Database/ILive.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; + +namespace osu.Game.Database +{ + /// + /// A wrapper to provide access to database backed classes in a thread-safe manner. + /// + /// The databased type. + public interface ILive where T : class + { + Guid ID { get; } + + /// + /// Perform a read operation on this live object. + /// + /// The action to perform. + void PerformRead(Action perform); + + /// + /// Perform a read operation on this live object. + /// + /// The action to perform. + TReturn PerformRead(Func perform); + + /// + /// Perform a write operation on this live object. + /// + /// The action to perform. + void PerformWrite(Action perform); + + /// + /// Resolve the value of this instance on the current thread's context. + /// + /// + /// After resolving the data should not be passed between threads. + /// + T Value { get; } + } +} diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs index fa3b4d9152..e94af01772 100644 --- a/osu.Game/Database/IModelImporter.cs +++ b/osu.Game/Database/IModelImporter.cs @@ -28,7 +28,7 @@ namespace osu.Game.Database Task Import(params ImportTask[] tasks); - Task> Import(ProgressNotification notification, params ImportTask[] tasks); + Task>> Import(ProgressNotification notification, params ImportTask[] tasks); /// /// Import one from the filesystem and delete the file on success. @@ -38,7 +38,7 @@ namespace osu.Game.Database /// Whether this is a low priority import. /// An optional cancellation token. /// The imported model, if successful. - Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default); + Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default); /// /// Silently import an item from an . @@ -46,7 +46,7 @@ namespace osu.Game.Database /// The archive to be imported. /// Whether this is a low priority import. /// An optional cancellation token. - Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default); + Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default); /// /// Silently import an item from a . @@ -55,7 +55,7 @@ namespace osu.Game.Database /// An optional archive to use for model population. /// Whether this is a low priority import. /// An optional cancellation token. - Task Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); + Task> Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default); /// /// A user displayable name for the model type associated with this manager. diff --git a/osu.Game/Database/IPresentImports.cs b/osu.Game/Database/IPresentImports.cs index 39b495ebd5..6aa29a5083 100644 --- a/osu.Game/Database/IPresentImports.cs +++ b/osu.Game/Database/IPresentImports.cs @@ -12,6 +12,6 @@ namespace osu.Game.Database /// /// Fired when the user requests to view the resulting import. /// - public Action> PresentImport { set; } + public Action>> PresentImport { set; } } } diff --git a/osu.Game/FodyWeavers.xml b/osu.Game/FodyWeavers.xml new file mode 100644 index 0000000000..cc07b89533 --- /dev/null +++ b/osu.Game/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 99925bb1fb..35ec213755 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -624,10 +624,10 @@ namespace osu.Game SkinManager.PostNotification = n => Notifications.Post(n); BeatmapManager.PostNotification = n => Notifications.Post(n); - BeatmapManager.PresentImport = items => PresentBeatmap(items.First()); + BeatmapManager.PresentImport = items => PresentBeatmap(items.First().Value); ScoreManager.PostNotification = n => Notifications.Post(n); - ScoreManager.PresentImport = items => PresentScore(items.First()); + ScoreManager.PresentImport = items => PresentScore(items.First().Value); // make config aware of how to lookup skins for on-screen display purposes. // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index d83b4e3f1d..aa0ee4bbbb 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -299,22 +299,22 @@ namespace osu.Game.Scoring public IEnumerable HandledExtensions => scoreModelManager.HandledExtensions; - public Task> Import(ProgressNotification notification, params ImportTask[] tasks) + public Task>> Import(ProgressNotification notification, params ImportTask[] tasks) { return scoreModelManager.Import(notification, tasks); } - public Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) { return scoreModelManager.Import(task, lowPriority, cancellationToken); } - public Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) { return scoreModelManager.Import(archive, lowPriority, cancellationToken); } - public Task Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) + public Task> Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { return scoreModelManager.Import(item, archive, lowPriority, cancellationToken); } @@ -365,7 +365,7 @@ namespace osu.Game.Scoring #region Implementation of IPresentImports - public Action> PresentImport + public Action>> PresentImport { set => scoreModelManager.PresentImport = value; } diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index cfe14eab92..fbd33cad67 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -101,8 +101,12 @@ namespace osu.Game.Screens.Menu // if we detect that the theme track or beatmap is unavailable this is either first startup or things are in a bad state. // this could happen if a user has nuked their files store. for now, reimport to repair this. var import = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream($"Tracks/{BeatmapFile}"), BeatmapFile)).Result; - import.Protected = true; - beatmaps.Update(import); + + import.PerformWrite(b => + { + b.Protected = true; + beatmaps.Update(b); + }); loadThemedIntro(); } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index edeb17cbad..3842acab74 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -207,7 +207,7 @@ namespace osu.Game.Skinning Name = skin.SkinInfo.Name + " (modified)", Creator = skin.SkinInfo.Creator, InstantiationInfo = skin.SkinInfo.InstantiationInfo, - }).Result; + }).Result.Value; } public void Save(Skin skin) From 9fa901f6aa28feb7183cba972930a99378c40daf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 23:42:40 +0900 Subject: [PATCH 104/391] Refine `RealmContext` implementation API --- .../Database/TestRealmKeyBindingStore.cs | 20 +- osu.Game/Database/IRealmFactory.cs | 12 +- osu.Game/Database/RealmContextFactory.cs | 257 ++++++------------ osu.Game/Database/RealmExtensions.cs | 45 +-- osu.Game/Database/RealmObjectExtensions.cs | 51 ++++ osu.Game/Input/RealmKeyBindingStore.cs | 20 +- osu.Game/OsuGameBase.cs | 11 +- .../Settings/Sections/Input/KeyBindingRow.cs | 8 +- .../Sections/Input/KeyBindingsSubsection.cs | 4 +- 9 files changed, 174 insertions(+), 254 deletions(-) create mode 100644 osu.Game/Database/RealmObjectExtensions.cs diff --git a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs index 8be74f1a7c..f10b11733e 100644 --- a/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs +++ b/osu.Game.Tests/Database/TestRealmKeyBindingStore.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Database storage = new NativeStorage(directory.FullName); - realmContextFactory = new RealmContextFactory(storage); + realmContextFactory = new RealmContextFactory(storage, "test"); keyBindingStore = new RealmKeyBindingStore(realmContextFactory); } @@ -53,9 +53,9 @@ namespace osu.Game.Tests.Database private int queryCount(GlobalAction? match = null) { - using (var usage = realmContextFactory.GetForRead()) + using (var realm = realmContextFactory.CreateContext()) { - var results = usage.Realm.All(); + var results = realm.All(); if (match.HasValue) results = results.Where(k => k.ActionInt == (int)match.Value); return results.Count(); @@ -69,26 +69,24 @@ namespace osu.Game.Tests.Database keyBindingStore.Register(testContainer, Enumerable.Empty()); - using (var primaryUsage = realmContextFactory.GetForRead()) + using (var primaryRealm = realmContextFactory.CreateContext()) { - var backBinding = primaryUsage.Realm.All().Single(k => k.ActionInt == (int)GlobalAction.Back); + var backBinding = primaryRealm.All().Single(k => k.ActionInt == (int)GlobalAction.Back); Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.Escape })); var tsr = ThreadSafeReference.Create(backBinding); - using (var usage = realmContextFactory.GetForWrite()) + using (var threadedContext = realmContextFactory.CreateContext()) { - var binding = usage.Realm.ResolveReference(tsr); - binding.KeyCombination = new KeyCombination(InputKey.BackSpace); - - usage.Commit(); + var binding = threadedContext.ResolveReference(tsr); + threadedContext.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace)); } Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); // check still correct after re-query. - backBinding = primaryUsage.Realm.All().Single(k => k.ActionInt == (int)GlobalAction.Back); + backBinding = primaryRealm.All().Single(k => k.ActionInt == (int)GlobalAction.Back); Assert.That(backBinding.KeyCombination.Keys, Is.EquivalentTo(new[] { InputKey.BackSpace })); } } diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs index 0e93e5bf4f..3b206d80eb 100644 --- a/osu.Game/Database/IRealmFactory.cs +++ b/osu.Game/Database/IRealmFactory.cs @@ -9,20 +9,12 @@ namespace osu.Game.Database { /// /// The main realm context, bound to the update thread. - /// If querying from a non-update thread is needed, use or to receive a context instead. /// Realm Context { get; } /// - /// Get a fresh context for read usage. + /// Create a new realm context for use on an arbitrary thread. /// - RealmContextFactory.RealmUsage GetForRead(); - - /// - /// Request a context for write usage. - /// This method may block if a write is already active on a different thread. - /// - /// A usage containing a usable context. - RealmContextFactory.RealmWriteUsage GetForWrite(); + Realm CreateContext(); } } diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index ed3dc01f15..c51ac095bb 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Development; @@ -10,80 +9,115 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; -using osu.Game.Input.Bindings; using Realms; +#nullable enable + namespace osu.Game.Database { + /// + /// A factory which provides both the main (update thread bound) realm context and creates contexts for async usage. + /// public class RealmContextFactory : Component, IRealmFactory { private readonly Storage storage; - private const string database_name = @"client"; + /// + /// The filename of this realm. + /// + public readonly string Filename; private const int schema_version = 6; /// - /// Lock object which is held for the duration of a write operation (via ). + /// Lock object which is held during sections, blocking context creation during blocking periods. /// - private readonly object writeLock = new object(); + private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1); - /// - /// Lock object which is held during sections. - /// - private readonly SemaphoreSlim blockingLock = new SemaphoreSlim(1); - - private static readonly GlobalStatistic reads = GlobalStatistics.Get("Realm", "Get (Read)"); - private static readonly GlobalStatistic writes = GlobalStatistics.Get("Realm", "Get (Write)"); private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Dirty Refreshes"); private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get("Realm", "Contexts (Created)"); - private static readonly GlobalStatistic pending_writes = GlobalStatistics.Get("Realm", "Pending writes"); - private static readonly GlobalStatistic active_usages = GlobalStatistics.Get("Realm", "Active usages"); - private readonly object updateContextLock = new object(); - - private Realm context; + private Realm? context; public Realm Context { get { if (!ThreadSafety.IsUpdateThread) - throw new InvalidOperationException($"Use {nameof(GetForRead)} or {nameof(GetForWrite)} when performing realm operations from a non-update thread"); + throw new InvalidOperationException($"Use {nameof(CreateContext)} when performing realm operations from a non-update thread"); - lock (updateContextLock) + if (context == null) { - if (context == null) - { - context = createContext(); - Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}"); - } - - // creating a context will ensure our schema is up-to-date and migrated. - - return context; + context = createContext(); + Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}"); } + + // creating a context will ensure our schema is up-to-date and migrated. + return context; } } - public RealmContextFactory(Storage storage) + public RealmContextFactory(Storage storage, string filename) { this.storage = storage; + + Filename = filename; + + const string realm_extension = ".realm"; + + if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal)) + Filename += realm_extension; } - public RealmUsage GetForRead() + public Realm CreateContext() { - reads.Value++; - return new RealmUsage(createContext()); + if (IsDisposed) + throw new ObjectDisposedException(nameof(RealmContextFactory)); + + return createContext(); } - public RealmWriteUsage GetForWrite() - { - writes.Value++; - pending_writes.Value++; + /// + /// Compact this realm. + /// + /// + public bool Compact() => Realm.Compact(getConfiguration()); - Monitor.Enter(writeLock); - return new RealmWriteUsage(createContext(), writeComplete); + protected override void Update() + { + base.Update(); + + if (context?.Refresh() == true) + refreshes.Value++; + } + + private Realm createContext() + { + try + { + contextCreationLock.Wait(); + + contexts_created.Value++; + + return Realm.GetInstance(getConfiguration()); + } + finally + { + contextCreationLock.Release(); + } + } + + private RealmConfiguration getConfiguration() + { + return new RealmConfiguration(storage.GetFullPath(Filename, true)) + { + SchemaVersion = schema_version, + MigrationCallback = onMigration, + }; + } + + private void onMigration(Migration migration, ulong lastSchemaVersion) + { } /// @@ -101,163 +135,32 @@ namespace osu.Game.Database Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); - blockingLock.Wait(); - flushContexts(); + contextCreationLock.Wait(); + + context?.Dispose(); + context = null; return new InvokeOnDisposal(this, endBlockingSection); static void endBlockingSection(RealmContextFactory factory) { - factory.blockingLock.Release(); + factory.contextCreationLock.Release(); Logger.Log(@"Restoring realm operations.", LoggingTarget.Database); } } - protected override void Update() - { - base.Update(); - - lock (updateContextLock) - { - if (context?.Refresh() == true) - refreshes.Value++; - } - } - - private Realm createContext() - { - try - { - if (IsDisposed) - throw new ObjectDisposedException(nameof(RealmContextFactory)); - - blockingLock.Wait(); - - contexts_created.Value++; - - return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true)) - { - SchemaVersion = schema_version, - MigrationCallback = onMigration, - }); - } - finally - { - blockingLock.Release(); - } - } - - private void writeComplete() - { - Monitor.Exit(writeLock); - pending_writes.Value--; - } - - private void onMigration(Migration migration, ulong lastSchemaVersion) - { - switch (lastSchemaVersion) - { - case 5: - // let's keep things simple. changing the type of the primary key is a bit involved. - migration.NewRealm.RemoveAll(); - break; - } - } - - private void flushContexts() - { - Logger.Log(@"Flushing realm contexts...", LoggingTarget.Database); - Debug.Assert(blockingLock.CurrentCount == 0); - - Realm previousContext; - - lock (updateContextLock) - { - previousContext = context; - context = null; - } - - // wait for all threaded usages to finish - while (active_usages.Value > 0) - Thread.Sleep(50); - - previousContext?.Dispose(); - - Logger.Log(@"Realm contexts flushed.", LoggingTarget.Database); - } - protected override void Dispose(bool isDisposing) { + context?.Dispose(); + if (!IsDisposed) { // intentionally block all operations indefinitely. this ensures that nothing can start consuming a new context after disposal. BlockAllOperations(); - blockingLock?.Dispose(); + contextCreationLock.Dispose(); } base.Dispose(isDisposing); } - - /// - /// A usage of realm from an arbitrary thread. - /// - public class RealmUsage : IDisposable - { - public readonly Realm Realm; - - internal RealmUsage(Realm context) - { - active_usages.Value++; - Realm = context; - } - - /// - /// Disposes this instance, calling the initially captured action. - /// - public virtual void Dispose() - { - Realm?.Dispose(); - active_usages.Value--; - } - } - - /// - /// A transaction used for making changes to realm data. - /// - public class RealmWriteUsage : RealmUsage - { - private readonly Action onWriteComplete; - private readonly Transaction transaction; - - internal RealmWriteUsage(Realm context, Action onWriteComplete) - : base(context) - { - this.onWriteComplete = onWriteComplete; - transaction = Realm.BeginWrite(); - } - - /// - /// Commit all changes made in this transaction. - /// - public void Commit() => transaction.Commit(); - - /// - /// Revert all changes made in this transaction. - /// - public void Rollback() => transaction.Rollback(); - - /// - /// Disposes this instance, calling the initially captured action. - /// - public override void Dispose() - { - // rollback if not explicitly committed. - transaction?.Dispose(); - - base.Dispose(); - - onWriteComplete(); - } - } } } diff --git a/osu.Game/Database/RealmExtensions.cs b/osu.Game/Database/RealmExtensions.cs index aee36e81c5..e6f3dba39f 100644 --- a/osu.Game/Database/RealmExtensions.cs +++ b/osu.Game/Database/RealmExtensions.cs @@ -1,51 +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 AutoMapper; -using osu.Game.Input.Bindings; +using System; using Realms; namespace osu.Game.Database { public static class RealmExtensions { - private static readonly IMapper mapper = new MapperConfiguration(c => + public static void Write(this Realm realm, Action function) { - c.ShouldMapField = fi => false; - c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic; - - c.CreateMap(); - }).CreateMapper(); - - /// - /// Create a detached copy of the each item in the collection. - /// - /// A list of managed s to detach. - /// The type of object. - /// A list containing non-managed copies of provided items. - public static List Detach(this IEnumerable items) where T : RealmObject - { - var list = new List(); - - foreach (var obj in items) - list.Add(obj.Detach()); - - return list; + using var transaction = realm.BeginWrite(); + function(realm); + transaction.Commit(); } - /// - /// Create a detached copy of the item. - /// - /// The managed to detach. - /// The type of object. - /// A non-managed copy of provided item. Will return the provided item if already detached. - public static T Detach(this T item) where T : RealmObject + public static T Write(this Realm realm, Func function) { - if (!item.IsManaged) - return item; - - return mapper.Map(item); + using var transaction = realm.BeginWrite(); + var result = function(realm); + transaction.Commit(); + return result; } } } diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs new file mode 100644 index 0000000000..c5aa1399a3 --- /dev/null +++ b/osu.Game/Database/RealmObjectExtensions.cs @@ -0,0 +1,51 @@ +// 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 AutoMapper; +using osu.Game.Input.Bindings; +using Realms; + +namespace osu.Game.Database +{ + public static class RealmObjectExtensions + { + private static readonly IMapper mapper = new MapperConfiguration(c => + { + c.ShouldMapField = fi => false; + c.ShouldMapProperty = pi => pi.SetMethod != null && pi.SetMethod.IsPublic; + + c.CreateMap(); + }).CreateMapper(); + + /// + /// Create a detached copy of the each item in the collection. + /// + /// A list of managed s to detach. + /// The type of object. + /// A list containing non-managed copies of provided items. + public static List Detach(this IEnumerable items) where T : RealmObject + { + var list = new List(); + + foreach (var obj in items) + list.Add(obj.Detach()); + + return list; + } + + /// + /// Create a detached copy of the item. + /// + /// The managed to detach. + /// The type of object. + /// A non-managed copy of provided item. Will return the provided item if already detached. + public static T Detach(this T item) where T : RealmObject + { + if (!item.IsManaged) + return item; + + return mapper.Map(item); + } + } +} diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 03cb4031ca..5fa3ccdeb9 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -7,6 +7,7 @@ using osu.Framework.Input.Bindings; using osu.Game.Database; using osu.Game.Input.Bindings; using osu.Game.Rulesets; +using Realms; #nullable enable @@ -30,9 +31,9 @@ namespace osu.Game.Input { List combinations = new List(); - using (var context = realmFactory.GetForRead()) + using (var context = realmFactory.CreateContext()) { - foreach (var action in context.Realm.All().Where(b => b.RulesetID == null && (GlobalAction)b.ActionInt == globalAction)) + foreach (var action in context.All().Where(b => b.RulesetID == null && (GlobalAction)b.ActionInt == globalAction)) { string str = action.KeyCombination.ReadableString(); @@ -52,26 +53,27 @@ namespace osu.Game.Input /// The rulesets to populate defaults from. public void Register(KeyBindingContainer container, IEnumerable rulesets) { - using (var usage = realmFactory.GetForWrite()) + using (var realm = realmFactory.CreateContext()) + using (var transaction = realm.BeginWrite()) { // intentionally flattened to a list rather than querying against the IQueryable, as nullable fields being queried against aren't indexed. // this is much faster as a result. - var existingBindings = usage.Realm.All().ToList(); + var existingBindings = realm.All().ToList(); - insertDefaults(usage, existingBindings, container.DefaultKeyBindings); + insertDefaults(realm, existingBindings, container.DefaultKeyBindings); foreach (var ruleset in rulesets) { var instance = ruleset.CreateInstance(); foreach (var variant in instance.AvailableVariants) - insertDefaults(usage, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ID, variant); + insertDefaults(realm, existingBindings, instance.GetDefaultKeyBindings(variant), ruleset.ID, variant); } - usage.Commit(); + transaction.Commit(); } } - private void insertDefaults(RealmContextFactory.RealmUsage usage, List existingBindings, IEnumerable defaults, int? rulesetId = null, int? variant = null) + private void insertDefaults(Realm realm, List existingBindings, IEnumerable defaults, int? rulesetId = null, int? variant = null) { // compare counts in database vs defaults for each action type. foreach (var defaultsForAction in defaults.GroupBy(k => k.Action)) @@ -83,7 +85,7 @@ namespace osu.Game.Input continue; // insert any defaults which are missing. - usage.Realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding + realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding { KeyCombinationString = k.KeyCombination.ToString(), ActionInt = (int)k.Action, diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 7aa460981a..f8f39029d2 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -187,7 +187,7 @@ namespace osu.Game dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); - dependencies.Cache(realmFactory = new RealmContextFactory(Storage)); + dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client")); updateThreadState = Host.UpdateThread.State.GetBoundCopy(); updateThreadState.BindValueChanged(updateThreadStateChanged); @@ -448,19 +448,20 @@ namespace osu.Game private void migrateDataToRealm() { using (var db = contextFactory.GetForWrite()) - using (var usage = realmFactory.GetForWrite()) + using (var realm = realmFactory.CreateContext()) + using (var transaction = realm.BeginWrite()) { // migrate ruleset settings. can be removed 20220315. var existingSettings = db.Context.DatabasedSetting; // only migrate data if the realm database is empty. - if (!usage.Realm.All().Any()) + if (!realm.All().Any()) { foreach (var dkb in existingSettings) { if (dkb.RulesetID == null) continue; - usage.Realm.Add(new RealmRulesetSetting + realm.Add(new RealmRulesetSetting { Key = dkb.Key, Value = dkb.StringValue, @@ -472,7 +473,7 @@ namespace osu.Game db.Context.RemoveRange(existingSettings); - usage.Commit(); + transaction.Commit(); } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index 85d88c96f8..cf8adf2785 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -368,12 +368,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input private void updateStoreFromButton(KeyButton button) { - using (var usage = realmFactory.GetForWrite()) + using (var realm = realmFactory.CreateContext()) { - var binding = usage.Realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); - binding.KeyCombinationString = button.KeyBinding.KeyCombinationString; - - usage.Commit(); + var binding = realm.Find(((IHasGuidPrimaryKey)button.KeyBinding).ID); + realm.Write(() => binding.KeyCombinationString = button.KeyBinding.KeyCombinationString); } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index fae0318359..0e8e10c086 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -38,8 +38,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input List bindings; - using (var usage = realmFactory.GetForRead()) - bindings = usage.Realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); + using (var realm = realmFactory.CreateContext()) + bindings = realm.All().Where(b => b.RulesetID == rulesetId && b.Variant == variant).Detach(); foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { From 38cd383aaf474f91fa7668718de9923f4fd087cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 00:27:54 +0900 Subject: [PATCH 105/391] Remove local handling of realm when switching thread modes --- osu.Game/OsuGameBase.cs | 37 +++++++------------------------------ 1 file changed, 7 insertions(+), 30 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 7aa460981a..d8cf8c729e 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -13,24 +13,23 @@ using osu.Framework.Development; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.IO.Stores; -using osu.Framework.Platform; -using osu.Game.Beatmaps; -using osu.Game.Configuration; -using osu.Game.Graphics; -using osu.Game.Graphics.Cursor; -using osu.Game.Online.API; using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Textures; using osu.Framework.Input; +using osu.Framework.IO.Stores; using osu.Framework.Logging; -using osu.Framework.Threading; +using osu.Framework.Platform; using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Cursor; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.Chat; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; @@ -160,8 +159,6 @@ namespace osu.Game private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(GLOBAL_TRACK_VOLUME_ADJUST); - private IBindable updateThreadState; - public OsuGameBase() { UseDevelopmentServer = DebugUtils.IsDebugBuild; @@ -189,9 +186,6 @@ namespace osu.Game dependencies.Cache(realmFactory = new RealmContextFactory(Storage)); - updateThreadState = Host.UpdateThread.State.GetBoundCopy(); - updateThreadState.BindValueChanged(updateThreadStateChanged); - AddInternal(realmFactory); dependencies.CacheAs(Storage); @@ -367,23 +361,6 @@ namespace osu.Game AddFont(Resources, @"Fonts/Venera/Venera-Black"); } - private IDisposable blocking; - - private void updateThreadStateChanged(ValueChangedEvent state) - { - switch (state.NewValue) - { - case GameThreadState.Running: - blocking?.Dispose(); - blocking = null; - break; - - case GameThreadState.Paused: - blocking = realmFactory.BlockAllOperations(); - break; - } - } - protected override void LoadComplete() { base.LoadComplete(); From ca7346e01fc5280ebe53106146f4434958c75c4d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 00:34:09 +0900 Subject: [PATCH 106/391] Add test coverage --- osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs index e2baa82ba0..7327d4053a 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; @@ -123,6 +124,13 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("ruleset unchanged", () => ReferenceEquals(Ruleset.Value, ruleset)); } + [Test] + public void TestSwitchThreadExecutionMode() + { + AddStep("Change thread mode to multi threaded", () => { game.Dependencies.Get().SetValue(FrameworkSetting.ExecutionMode, ExecutionMode.MultiThreaded); }); + AddStep("Change thread mode to single thread", () => { game.Dependencies.Get().SetValue(FrameworkSetting.ExecutionMode, ExecutionMode.SingleThread); }); + } + [Test] public void TestUnavailableRulesetHandled() { From 9c0abae2b0836dd7cb9e3584be85ccf34ad03615 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 23:59:26 +0900 Subject: [PATCH 107/391] Add failing test coverage of realm blocking behaviour --- osu.Game.Tests/Database/GeneralUsageTests.cs | 64 ++++++++++++++++++ osu.Game.Tests/Database/RealmTest.cs | 70 ++++++++++++++++++++ osu.Game.Tests/osu.Game.Tests.csproj | 1 + 3 files changed, 135 insertions(+) create mode 100644 osu.Game.Tests/Database/GeneralUsageTests.cs create mode 100644 osu.Game.Tests/Database/RealmTest.cs diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs new file mode 100644 index 0000000000..245981cd9b --- /dev/null +++ b/osu.Game.Tests/Database/GeneralUsageTests.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; + +#nullable enable + +namespace osu.Game.Tests.Database +{ + [TestFixture] + public class GeneralUsageTests : RealmTest + { + /// + /// Just test the construction of a new database works. + /// + [Test] + public void TestConstructRealm() + { + RunTestWithRealm((realmFactory, _) => { realmFactory.CreateContext().Refresh(); }); + } + + [Test] + public void TestBlockOperations() + { + RunTestWithRealm((realmFactory, _) => + { + using (realmFactory.BlockAllOperations()) + { + } + }); + } + + [Test] + public void TestBlockOperationsWithContention() + { + RunTestWithRealm((realmFactory, _) => + { + ManualResetEventSlim stopThreadedUsage = new ManualResetEventSlim(); + ManualResetEventSlim hasThreadedUsage = new ManualResetEventSlim(); + + Task.Factory.StartNew(() => + { + using (realmFactory.CreateContext()) + { + hasThreadedUsage.Set(); + + stopThreadedUsage.Wait(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler); + + hasThreadedUsage.Wait(); + + Assert.Throws(() => + { + using (realmFactory.BlockAllOperations()) + { + } + }); + + stopThreadedUsage.Set(); + }); + } + } +} diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs new file mode 100644 index 0000000000..2f4838cb67 --- /dev/null +++ b/osu.Game.Tests/Database/RealmTest.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.Runtime.CompilerServices; +using System.Threading.Tasks; +using Nito.AsyncEx; +using NUnit.Framework; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Database; + +#nullable enable + +namespace osu.Game.Tests.Database +{ + [TestFixture] + public abstract class RealmTest + { + private static readonly TemporaryNativeStorage storage; + + static RealmTest() + { + storage = new TemporaryNativeStorage("realm-test"); + storage.DeleteDirectory(string.Empty); + } + + protected void RunTestWithRealm(Action testAction, [CallerMemberName] string caller = "") + { + AsyncContext.Run(() => + { + var testStorage = storage.GetStorageForDirectory(caller); + + using (var realmFactory = new RealmContextFactory(testStorage, caller)) + { + Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); + testAction(realmFactory, testStorage); + + realmFactory.Dispose(); + Logger.Log($"Final database size: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + + realmFactory.Compact(); + Logger.Log($"Final database size after compact: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + } + }); + } + + protected void RunTestWithRealmAsync(Func testAction, [CallerMemberName] string caller = "") + { + AsyncContext.Run(async () => + { + var testStorage = storage.GetStorageForDirectory(caller); + + using (var realmFactory = new RealmContextFactory(testStorage, caller)) + { + Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); + + await testAction(realmFactory, testStorage); + + realmFactory.Dispose(); + Logger.Log($"Final database size: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + + realmFactory.Compact(); + Logger.Log($"Final database size after compact: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + } + }); + } + } +} diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 696f930467..cd56cb51ae 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -4,6 +4,7 @@ + From cfd3bdf888fc24df4bc8eeb0f8def24471352bbb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 01:32:28 +0900 Subject: [PATCH 108/391] Ensure realm blocks until all threaded usages are completed --- osu.Game/Database/RealmContextFactory.cs | 39 +++++++++++++++++++----- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index c51ac095bb..e3b0764721 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -133,20 +133,43 @@ namespace osu.Game.Database if (IsDisposed) throw new ObjectDisposedException(nameof(RealmContextFactory)); + // TODO: this can be added for safety once we figure how to bypass in test + // if (!ThreadSafety.IsUpdateThread) + // throw new InvalidOperationException($"{nameof(BlockAllOperations)} must be called from the update thread."); + Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); - contextCreationLock.Wait(); + try + { + contextCreationLock.Wait(); - context?.Dispose(); - context = null; + const int sleep_length = 200; + int timeout = 5000; - return new InvokeOnDisposal(this, endBlockingSection); + context?.Dispose(); + context = null; - static void endBlockingSection(RealmContextFactory factory) + // see https://github.com/realm/realm-dotnet/discussions/2657 + while (!Compact()) + { + Thread.Sleep(sleep_length); + timeout -= sleep_length; + + if (timeout < 0) + throw new TimeoutException("Took too long to acquire lock"); + } + } + catch + { + contextCreationLock.Release(); + throw; + } + + return new InvokeOnDisposal(this, factory => { factory.contextCreationLock.Release(); Logger.Log(@"Restoring realm operations.", LoggingTarget.Database); - } + }); } protected override void Dispose(bool isDisposing) @@ -155,8 +178,8 @@ namespace osu.Game.Database if (!IsDisposed) { - // intentionally block all operations indefinitely. this ensures that nothing can start consuming a new context after disposal. - BlockAllOperations(); + // intentionally block context creation indefinitely. this ensures that nothing can start consuming a new context after disposal. + contextCreationLock.Wait(); contextCreationLock.Dispose(); } From dde19f2e81ca08df3328799d6d244ae4e62ed9cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 01:37:51 +0900 Subject: [PATCH 109/391] Fix unbalanced brackets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index 19f02c82ec..bc86c6be5d 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -211,7 +211,7 @@ namespace osu.Game.Beatmaps } private void logForModel(BeatmapSetInfo set, string message) => - ArchiveModelManager.LogForModel(set, $"{nameof(BeatmapOnlineLookupQueue)}] {message}"); + ArchiveModelManager.LogForModel(set, $"[{nameof(BeatmapOnlineLookupQueue)}] {message}"); public void Dispose() { From 27c4f2b06ee70adab63aeabfa03d42cdcbfbeefc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 01:38:50 +0900 Subject: [PATCH 110/391] Add missing disposal --- osu.Game/OsuGameBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8263e26dec..f239119e40 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -531,6 +531,7 @@ namespace osu.Game RulesetStore?.Dispose(); LocalConfig?.Dispose(); + onlineBeatmapLookupCache?.Dispose(); contextFactory?.FlushConnections(); } From 428c7830d958d731398bcb18193160461418a670 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 01:43:57 +0900 Subject: [PATCH 111/391] Pass online lookup queue in as a whole, rather than function --- osu.Game/Beatmaps/BeatmapManager.cs | 16 +++++++++++++--- osu.Game/Beatmaps/BeatmapModelManager.cs | 9 ++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index c72d1e8dec..1946e3f93f 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -29,10 +29,11 @@ namespace osu.Game.Beatmaps /// Handles general operations related to global beatmap management. /// [ExcludeFromDynamicCompile] - public class BeatmapManager : IModelDownloader, IModelManager, IModelFileManager, ICanAcceptFiles, IWorkingBeatmapCache + public class BeatmapManager : IModelDownloader, IModelManager, IModelFileManager, ICanAcceptFiles, IWorkingBeatmapCache, IDisposable { private readonly BeatmapModelManager beatmapModelManager; private readonly WorkingBeatmapCache workingBeatmapCache; + private readonly BeatmapOnlineLookupQueue onlineBetamapLookupQueue; public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) @@ -44,8 +45,8 @@ namespace osu.Game.Beatmaps if (performOnlineLookups) { - var onlineBeatmapLookupCache = new BeatmapOnlineLookupQueue(api, storage); - beatmapModelManager.PopulateOnlineInformation = onlineBeatmapLookupCache.UpdateAsync; + onlineBetamapLookupQueue = new BeatmapOnlineLookupQueue(api, storage); + beatmapModelManager.OnlineLookupQueue = onlineBetamapLookupQueue; } } @@ -308,5 +309,14 @@ namespace osu.Game.Beatmaps } #endregion + + #region Implementation of IDisposable + + public void Dispose() + { + onlineBetamapLookupQueue?.Dispose(); + } + + #endregion } } diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index be3adc412c..72df1f37ee 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -49,10 +49,9 @@ namespace osu.Game.Beatmaps public IBindable> BeatmapRestored => beatmapRestored; /// - /// A function which populates online information during the import process. - /// It is run as the final step of import. + /// An online lookup queue component which handles populating online beatmap metadata. /// - public Func PopulateOnlineInformation; + public BeatmapOnlineLookupQueue OnlineLookupQueue { private get; set; } /// /// The game working beatmap cache, used to invalidate entries on changes. @@ -107,8 +106,8 @@ namespace osu.Game.Beatmaps bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); - if (PopulateOnlineInformation != null) - await PopulateOnlineInformation(beatmapSet, cancellationToken).ConfigureAwait(false); + if (OnlineLookupQueue != null) + await OnlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false); // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) From 2ed28f625a6ce106ad76c86b2772b808daf975dc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 01:46:37 +0900 Subject: [PATCH 112/391] Pass whole queue in rather than function --- osu.Game/Beatmaps/BeatmapManager.cs | 9 ++++----- osu.Game/OsuGameBase.cs | 8 ++++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index a2f9740779..1fc7aa3146 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -55,10 +55,9 @@ namespace osu.Game.Beatmaps public IBindable> BeatmapRestored => beatmapRestored; /// - /// A function which populates online information during the import process. - /// It is run as the final step of import. + /// An online lookup queue component which handles populating online beatmap metadata. /// - public Func PopulateOnlineInformation; + public BeatmapOnlineLookupQueue OnlineLookupQueue { private get; set; } private readonly Bindable> beatmapRestored = new Bindable>(); @@ -156,8 +155,8 @@ namespace osu.Game.Beatmaps bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); - if (PopulateOnlineInformation != null) - await PopulateOnlineInformation(beatmapSet, cancellationToken).ConfigureAwait(false); + if (OnlineLookupQueue != null) + await OnlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false); // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index f239119e40..7772d5dfd8 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -138,7 +138,7 @@ namespace osu.Game private UserLookupCache userCache; - private BeatmapOnlineLookupQueue onlineBeatmapLookupCache; + private BeatmapOnlineLookupQueue onlineBeatmapLookupQueue; private FileStore fileStore; @@ -246,9 +246,9 @@ namespace osu.Game dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Scheduler, Host, () => difficultyCache, LocalConfig)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap)); - onlineBeatmapLookupCache = new BeatmapOnlineLookupQueue(API, Storage); + onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue(API, Storage); - BeatmapManager.PopulateOnlineInformation = onlineBeatmapLookupCache.UpdateAsync; + BeatmapManager.OnlineLookupQueue = onlineBeatmapLookupQueue; // this should likely be moved to ArchiveModelManager when another case appears where it is necessary // to have inter-dependent model managers. this could be obtained with an IHasForeign interface to @@ -531,7 +531,7 @@ namespace osu.Game RulesetStore?.Dispose(); LocalConfig?.Dispose(); - onlineBeatmapLookupCache?.Dispose(); + onlineBeatmapLookupQueue?.Dispose(); contextFactory?.FlushConnections(); } From c71cf1e2200bcbefb2d71400782f90ba50918bd1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 01:51:29 +0900 Subject: [PATCH 113/391] Fix incomplete xmldoc --- osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index bc86c6be5d..55164e2442 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps /// A component which handles population of online IDs for beatmaps using a two part lookup procedure. /// /// - /// On creating the component, a copy of a database containing metadata for a large subset of beatmaps (stored to ). + /// On creating the component, a copy of a database containing metadata for a large subset of beatmaps (stored to ) will be downloaded if not already present locally. /// This will always be checked before doing a second online query to get required metadata. /// [ExcludeFromDynamicCompile] From 8557530cd5e744110b13a167ad30306c7224823e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 03:45:00 +0900 Subject: [PATCH 114/391] Add back main context locking --- osu.Game/Database/RealmContextFactory.cs | 30 ++++++++++++++++-------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index c51ac095bb..0e18b68276 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -37,6 +37,7 @@ namespace osu.Game.Database private static readonly GlobalStatistic refreshes = GlobalStatistics.Get("Realm", "Dirty Refreshes"); private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get("Realm", "Contexts (Created)"); + private readonly object contextLock = new object(); private Realm? context; public Realm Context @@ -46,14 +47,17 @@ namespace osu.Game.Database if (!ThreadSafety.IsUpdateThread) throw new InvalidOperationException($"Use {nameof(CreateContext)} when performing realm operations from a non-update thread"); - if (context == null) + lock (contextLock) { - context = createContext(); - Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}"); - } + if (context == null) + { + context = createContext(); + Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}"); + } - // creating a context will ensure our schema is up-to-date and migrated. - return context; + // creating a context will ensure our schema is up-to-date and migrated. + return context; + } } } @@ -87,8 +91,11 @@ namespace osu.Game.Database { base.Update(); - if (context?.Refresh() == true) - refreshes.Value++; + lock (contextLock) + { + if (context?.Refresh() == true) + refreshes.Value++; + } } private Realm createContext() @@ -137,8 +144,11 @@ namespace osu.Game.Database contextCreationLock.Wait(); - context?.Dispose(); - context = null; + lock (contextLock) + { + context?.Dispose(); + context = null; + } return new InvokeOnDisposal(this, endBlockingSection); From b51fd00ba34a8c201e79310834ca8e9ded9aeb4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 03:46:53 +0900 Subject: [PATCH 115/391] Guard against disposal in all context retrievals --- osu.Game/Database/RealmContextFactory.cs | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 0e18b68276..bf7feebdbf 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -51,7 +51,7 @@ namespace osu.Game.Database { if (context == null) { - context = createContext(); + context = CreateContext(); Logger.Log($"Opened realm \"{context.Config.DatabasePath}\" at version {context.Config.SchemaVersion}"); } @@ -73,14 +73,6 @@ namespace osu.Game.Database Filename += realm_extension; } - public Realm CreateContext() - { - if (IsDisposed) - throw new ObjectDisposedException(nameof(RealmContextFactory)); - - return createContext(); - } - /// /// Compact this realm. /// @@ -98,8 +90,11 @@ namespace osu.Game.Database } } - private Realm createContext() + public Realm CreateContext() { + if (IsDisposed) + throw new ObjectDisposedException(nameof(RealmContextFactory)); + try { contextCreationLock.Wait(); @@ -161,7 +156,10 @@ namespace osu.Game.Database protected override void Dispose(bool isDisposing) { - context?.Dispose(); + lock (contextLock) + { + context?.Dispose(); + } if (!IsDisposed) { From b5345235cae7edb8430f31e97139f1bbe016ee95 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 10:40:55 +0900 Subject: [PATCH 116/391] Handle window file access errors --- osu.Game.Tests/Database/RealmTest.cs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs index 2f4838cb67..b7658d6408 100644 --- a/osu.Game.Tests/Database/RealmTest.cs +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -38,10 +38,26 @@ namespace osu.Game.Tests.Database testAction(realmFactory, testStorage); realmFactory.Dispose(); - Logger.Log($"Final database size: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + + try + { + Logger.Log($"Final database size: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + } + catch + { + // windows runs may error due to file still being open. + } realmFactory.Compact(); - Logger.Log($"Final database size after compact: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + + try + { + Logger.Log($"Final database size after compact: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + } + catch + { + // windows runs may error due to file still being open. + } } }); } From 619dfe06907d74d3054dbdb3f23c9d0444b505d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 14:34:11 +0900 Subject: [PATCH 117/391] Add new interface base types for models --- osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs | 90 +++++++++++++++ osu.Game/Beatmaps/IBeatmapInfo.cs | 74 +++++++++++++ osu.Game/Beatmaps/IBeatmapMetadataInfo.cs | 117 ++++++++++++++++++++ osu.Game/Beatmaps/IBeatmapSetInfo.cs | 57 ++++++++++ osu.Game/Beatmaps/IFileInfo.cs | 18 +++ osu.Game/Beatmaps/IHasOnlineID.cs | 15 +++ osu.Game/Beatmaps/INamedFileUsage.cs | 23 ++++ osu.Game/Beatmaps/IRulesetInfo.cs | 46 ++++++++ 8 files changed, 440 insertions(+) create mode 100644 osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs create mode 100644 osu.Game/Beatmaps/IBeatmapInfo.cs create mode 100644 osu.Game/Beatmaps/IBeatmapMetadataInfo.cs create mode 100644 osu.Game/Beatmaps/IBeatmapSetInfo.cs create mode 100644 osu.Game/Beatmaps/IFileInfo.cs create mode 100644 osu.Game/Beatmaps/IHasOnlineID.cs create mode 100644 osu.Game/Beatmaps/INamedFileUsage.cs create mode 100644 osu.Game/Beatmaps/IRulesetInfo.cs diff --git a/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs new file mode 100644 index 0000000000..6d9fcfcb06 --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.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 enable + +namespace osu.Game.Beatmaps +{ + /// + /// A representation of all top-level difficulty settings for a beatmap. + /// + public interface IBeatmapDifficultyInfo + { + /// + /// The default value used for all difficulty settings except and . + /// + const float DEFAULT_DIFFICULTY = 5; + + /// + /// The drain rate of the associated beatmap. + /// + float DrainRate { get; } + + /// + /// The circle size of the associated beatmap. + /// + float CircleSize { get; } + + /// + /// The overall difficulty of the associated beatmap. + /// + float OverallDifficulty { get; } + + /// + /// The approach rate of the associated beatmap. + /// + float ApproachRate { get; } + + /// + /// The slider multiplier of the associated beatmap. + /// + double SliderMultiplier { get; } + + /// + /// The slider tick rate of the associated beatmap. + /// + double SliderTickRate { get; } + + /// + /// Maps a difficulty value [0, 10] to a two-piece linear range of values. + /// + /// The difficulty value to be mapped. + /// Minimum of the resulting range which will be achieved by a difficulty value of 0. + /// Midpoint of the resulting range which will be achieved by a difficulty value of 5. + /// Maximum of the resulting range which will be achieved by a difficulty value of 10. + /// Value to which the difficulty value maps in the specified range. + static double DifficultyRange(double difficulty, double min, double mid, double max) + { + if (difficulty > 5) + return mid + (max - mid) * (difficulty - 5) / 5; + if (difficulty < 5) + return mid - (mid - min) * (5 - difficulty) / 5; + + return mid; + } + + /// + /// Maps a difficulty value [0, 10] to a two-piece linear range of values. + /// + /// The difficulty value to be mapped. + /// The values that define the two linear ranges. + /// + /// + /// od0 + /// Minimum of the resulting range which will be achieved by a difficulty value of 0. + /// + /// + /// od5 + /// Midpoint of the resulting range which will be achieved by a difficulty value of 5. + /// + /// + /// od10 + /// Maximum of the resulting range which will be achieved by a difficulty value of 10. + /// + /// + /// + /// Value to which the difficulty value maps in the specified range. + public static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range) + => DifficultyRange(difficulty, range.od0, range.od5, range.od10); + } +} diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs new file mode 100644 index 0000000000..72a04621f2 --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapInfo.cs @@ -0,0 +1,74 @@ +// 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; + +#nullable enable + +namespace osu.Game.Beatmaps +{ + /// + /// A single beatmap difficulty. + /// + public interface IBeatmapInfo : IHasOnlineID + { + /// + /// The user-specified name given to this beatmap. + /// + string DifficultyName { get; } + + /// + /// The metadata representing this beatmap. May be shared between multiple beatmaps. + /// + IBeatmapMetadataInfo Metadata { get; } + + /// + /// The difficulty settings for this beatmap. + /// + IBeatmapDifficultyInfo Difficulty { get; } + + /// + /// The playable length in milliseconds of this beatmap. + /// + double Length { get; } + + /// + /// The most common BPM of this beatmap. + /// + double BPM { get; } + + /// + /// The SHA-256 hash representing this beatmap's contents. + /// + string Hash { get; } + + /// + /// MD5 is kept for legacy support (matching against replays, osu-web-10 etc.). + /// + string MD5Hash { get; } + + /// + /// The ruleset this beatmap was made for. + /// + IRulesetInfo Ruleset { get; } + + /// + /// The basic star rating for this beatmap (with no mods applied). + /// + double StarRating { get; } + + string DisplayTitle => $"{Metadata} {versionString}".Trim(); + + RomanisableString DisplayTitleRomanisable + { + get + { + var metadata = Metadata.DisplayTitleRomanisable; + + return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim()); + } + } + + private string versionString => string.IsNullOrEmpty(DifficultyName) ? string.Empty : $"[{DifficultyName}]"; + } +} diff --git a/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs b/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs new file mode 100644 index 0000000000..18dd38b404 --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapMetadataInfo.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.Linq; +using osu.Framework.Localisation; + +#nullable enable + +namespace osu.Game.Beatmaps +{ + /// + /// Metadata representing a beatmap. May be shared between multiple beatmap difficulties. + /// + public interface IBeatmapMetadataInfo : IEquatable + { + /// + /// The romanised title of this beatmap. + /// + string Title { get; } + + /// + /// The unicode title of this beatmap. + /// + string TitleUnicode { get; } + + /// + /// The romanised artist of this beatmap. + /// + string Artist { get; } + + /// + /// The unicode artist of this beatmap. + /// + string ArtistUnicode { get; } + + /// + /// The author of this beatmap. + /// + string Author { get; } // eventually should be linked to a persisted User. + + /// + /// The source of this beatmap. + /// + string Source { get; } + + /// + /// The tags of this beatmap. + /// + string Tags { get; } + + /// + /// The time in milliseconds to begin playing the track for preview purposes. + /// If -1, the track should begin playing at 40% of its length. + /// + int PreviewTime { get; } + + /// + /// The filename of the audio file consumed by this beatmap. + /// + string AudioFile { get; } + + /// + /// The filename of the background image file consumed by this beatmap. + /// + string BackgroundFile { get; } + + string DisplayTitle + { + get + { + string author = string.IsNullOrEmpty(Author) ? string.Empty : $"({Author})"; + return $"{Artist} - {Title} {author}".Trim(); + } + } + + RomanisableString DisplayTitleRomanisable + { + get + { + string author = string.IsNullOrEmpty(Author) ? string.Empty : $"({Author})"; + var artistUnicode = string.IsNullOrEmpty(ArtistUnicode) ? Artist : ArtistUnicode; + var titleUnicode = string.IsNullOrEmpty(TitleUnicode) ? Title : TitleUnicode; + + return new RomanisableString($"{artistUnicode} - {titleUnicode} {author}".Trim(), $"{Artist} - {Title} {author}".Trim()); + } + } + + string[] SearchableTerms => new[] + { + Author, + Artist, + ArtistUnicode, + Title, + TitleUnicode, + Source, + Tags + }.Where(s => !string.IsNullOrEmpty(s)).ToArray(); + + bool IEquatable.Equals(IBeatmapMetadataInfo? other) + { + if (other == null) + return false; + + return Title == other.Title + && TitleUnicode == other.TitleUnicode + && Artist == other.Artist + && ArtistUnicode == other.ArtistUnicode + && Author == other.Author + && Source == other.Source + && Tags == other.Tags + && PreviewTime == other.PreviewTime + && AudioFile == other.AudioFile + && BackgroundFile == other.BackgroundFile; + } + } +} diff --git a/osu.Game/Beatmaps/IBeatmapSetInfo.cs b/osu.Game/Beatmaps/IBeatmapSetInfo.cs new file mode 100644 index 0000000000..f22115e08f --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapSetInfo.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 System.Collections.Generic; +using System.Linq; + +#nullable enable + +namespace osu.Game.Beatmaps +{ + /// + /// A representation of a collection of beatmap difficulties, generally packaged as an ".osz" archive. + /// + public interface IBeatmapSetInfo : IHasOnlineID + { + /// + /// The date when this beatmap was imported. + /// + DateTimeOffset DateAdded { get; } + + /// + /// The best-effort metadata representing this set. In the case metadata differs between contained beatmaps, one is arbitrarily chosen. + /// + IBeatmapMetadataInfo? Metadata { get; } + + /// + /// All beatmaps contained in this set. + /// + IEnumerable Beatmaps { get; } + + /// + /// All files used by this set. + /// + IEnumerable Files { get; } + + /// + /// The maximum star difficulty of all beatmaps in this set. + /// + double MaxStarDifficulty { get; } + + /// + /// The maximum playable length in milliseconds of all beatmaps in this set. + /// + double MaxLength { get; } + + /// + /// The maximum BPM of all beatmaps in this set. + /// + double MaxBPM { get; } + + /// + /// The filename for the storyboard. + /// + string StoryboardFile => Files.FirstOrDefault(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename ?? string.Empty; + } +} diff --git a/osu.Game/Beatmaps/IFileInfo.cs b/osu.Game/Beatmaps/IFileInfo.cs new file mode 100644 index 0000000000..50eb223fc4 --- /dev/null +++ b/osu.Game/Beatmaps/IFileInfo.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. + +#nullable enable + +namespace osu.Game.Beatmaps +{ + /// + /// A representation of a tracked file. + /// + public interface IFileInfo + { + /// + /// SHA-256 hash of the file content. + /// + string Hash { get; } + } +} diff --git a/osu.Game/Beatmaps/IHasOnlineID.cs b/osu.Game/Beatmaps/IHasOnlineID.cs new file mode 100644 index 0000000000..dc2793afe5 --- /dev/null +++ b/osu.Game/Beatmaps/IHasOnlineID.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. + +#nullable enable + +namespace osu.Game.Beatmaps +{ + public interface IHasOnlineID + { + /// + /// The server-side ID representing this instance, if one exists. + /// + int? OnlineID { get; } + } +} diff --git a/osu.Game/Beatmaps/INamedFileUsage.cs b/osu.Game/Beatmaps/INamedFileUsage.cs new file mode 100644 index 0000000000..aa7a3852a7 --- /dev/null +++ b/osu.Game/Beatmaps/INamedFileUsage.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. + +#nullable enable + +namespace osu.Game.Beatmaps +{ + /// + /// A usage of a file, with a local filename attached. + /// + public interface INamedFileUsage + { + /// + /// The underlying file on disk. + /// + IFileInfo File { get; } + + /// + /// The filename for this usage. + /// + string Filename { get; } + } +} diff --git a/osu.Game/Beatmaps/IRulesetInfo.cs b/osu.Game/Beatmaps/IRulesetInfo.cs new file mode 100644 index 0000000000..b31ebdfbfd --- /dev/null +++ b/osu.Game/Beatmaps/IRulesetInfo.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 osu.Game.Rulesets; + +#nullable enable + +namespace osu.Game.Beatmaps +{ + /// + /// A representation of a ruleset's metadata. + /// + public interface IRulesetInfo : IHasOnlineID + { + /// + /// The user-exposed name of this ruleset. + /// + string Name { get; } + + /// + /// An acronym defined by the ruleset that can be used as a permanent identifier. + /// + string ShortName { get; } + + /// + /// A string representation of this ruleset, to be used with reflection to instantiate the ruleset represented by this metadata. + /// + string InstantiationInfo { get; } + + public Ruleset? CreateInstance() + { + var type = Type.GetType(InstantiationInfo); + + if (type == null) + return null; + + var ruleset = Activator.CreateInstance(type) as Ruleset; + + // overwrite the pre-populated RulesetInfo with a potentially database attached copy. + // ruleset.RulesetInfo = this; + + return ruleset; + } + } +} From d30963646077df2eb6f1d2377d7bd56585071e8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 16:31:11 +0900 Subject: [PATCH 118/391] Update all EF based models to implement new read only interfaces --- osu.Game/Beatmaps/BeatmapInfo.cs | 19 ++++++++++++++++++- osu.Game/Beatmaps/BeatmapMetadata.cs | 4 +++- osu.Game/Beatmaps/BeatmapSetFileInfo.cs | 4 +++- osu.Game/Beatmaps/BeatmapSetInfo.cs | 16 +++++++++++++++- osu.Game/Beatmaps/IBeatmapInfo.cs | 2 ++ osu.Game/Beatmaps/IBeatmapSetInfo.cs | 1 + .../{Beatmaps => Database}/IHasOnlineID.cs | 2 +- .../{Beatmaps => Database}/INamedFileUsage.cs | 4 +++- osu.Game/IO/FileInfo.cs | 2 +- osu.Game/{Beatmaps => IO}/IFileInfo.cs | 2 +- .../{Beatmaps => Rulesets}/IRulesetInfo.cs | 4 ++-- osu.Game/Rulesets/RulesetInfo.cs | 8 +++++++- 12 files changed, 57 insertions(+), 11 deletions(-) rename osu.Game/{Beatmaps => Database}/IHasOnlineID.cs (92%) rename osu.Game/{Beatmaps => Database}/INamedFileUsage.cs (92%) rename osu.Game/{Beatmaps => IO}/IFileInfo.cs (93%) rename osu.Game/{Beatmaps => Rulesets}/IRulesetInfo.cs (96%) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 8cb5da8083..d2b47ef1a4 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -17,13 +17,14 @@ namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] [Serializable] - public class BeatmapInfo : IEquatable, IHasPrimaryKey + public class BeatmapInfo : IEquatable, IHasPrimaryKey, IBeatmapInfo { public int ID { get; set; } public int BeatmapVersion; private int? onlineBeatmapID; + private IRulesetInfo ruleset; [JsonProperty("id")] public int? OnlineBeatmapID @@ -187,5 +188,21 @@ namespace osu.Game.Beatmaps /// Returns a shallow-clone of this . /// public BeatmapInfo Clone() => (BeatmapInfo)MemberwiseClone(); + + #region Implementation of IHasOnlineID + + public int? OnlineID => ID; + + #endregion + + #region Implementation of IBeatmapInfo + + public string DifficultyName => Version; + IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata; + public IBeatmapDifficultyInfo Difficulty => BaseDifficulty; + IRulesetInfo IBeatmapInfo.Ruleset => Ruleset; + public double StarRating => StarDifficulty; + + #endregion } } diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index 713f80d1fe..fbd47d2614 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -15,7 +15,7 @@ namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] [Serializable] - public class BeatmapMetadata : IEquatable, IHasPrimaryKey + public class BeatmapMetadata : IEquatable, IHasPrimaryKey, IBeatmapMetadataInfo { public int ID { get; set; } @@ -128,5 +128,7 @@ namespace osu.Game.Beatmaps && AudioFile == other.AudioFile && BackgroundFile == other.BackgroundFile; } + + string IBeatmapMetadataInfo.Author => AuthorString; } } diff --git a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs index 3a55dc1577..ce50463f05 100644 --- a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs @@ -7,7 +7,7 @@ using osu.Game.IO; namespace osu.Game.Beatmaps { - public class BeatmapSetFileInfo : INamedFileInfo, IHasPrimaryKey + public class BeatmapSetFileInfo : INamedFileInfo, IHasPrimaryKey, INamedFileUsage { public int ID { get; set; } @@ -19,5 +19,7 @@ namespace osu.Game.Beatmaps [Required] public string Filename { get; set; } + + public IFileInfo File => FileInfo; } } diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 3b1ff4ced0..739acb9a8d 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -12,7 +12,7 @@ using osu.Game.Database; namespace osu.Game.Beatmaps { [ExcludeFromDynamicCompile] - public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles, ISoftDelete, IEquatable + public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles, ISoftDelete, IEquatable, IBeatmapSetInfo { public int ID { get; set; } @@ -90,5 +90,19 @@ namespace osu.Game.Beatmaps return ReferenceEquals(this, other); } + + #region Implementation of IHasOnlineID + + public int? OnlineID => ID; + + #endregion + + #region Implementation of IBeatmapSetInfo + + IBeatmapMetadataInfo IBeatmapSetInfo.Metadata => Metadata; + IEnumerable IBeatmapSetInfo.Beatmaps => Beatmaps; + IEnumerable IBeatmapSetInfo.Files => Files; + + #endregion } } diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs index 72a04621f2..8ba8f316ed 100644 --- a/osu.Game/Beatmaps/IBeatmapInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapInfo.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Localisation; +using osu.Game.Database; +using osu.Game.Rulesets; #nullable enable diff --git a/osu.Game/Beatmaps/IBeatmapSetInfo.cs b/osu.Game/Beatmaps/IBeatmapSetInfo.cs index f22115e08f..548a48367c 100644 --- a/osu.Game/Beatmaps/IBeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapSetInfo.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Game.Database; #nullable enable diff --git a/osu.Game/Beatmaps/IHasOnlineID.cs b/osu.Game/Database/IHasOnlineID.cs similarity index 92% rename from osu.Game/Beatmaps/IHasOnlineID.cs rename to osu.Game/Database/IHasOnlineID.cs index dc2793afe5..c55c461d2d 100644 --- a/osu.Game/Beatmaps/IHasOnlineID.cs +++ b/osu.Game/Database/IHasOnlineID.cs @@ -3,7 +3,7 @@ #nullable enable -namespace osu.Game.Beatmaps +namespace osu.Game.Database { public interface IHasOnlineID { diff --git a/osu.Game/Beatmaps/INamedFileUsage.cs b/osu.Game/Database/INamedFileUsage.cs similarity index 92% rename from osu.Game/Beatmaps/INamedFileUsage.cs rename to osu.Game/Database/INamedFileUsage.cs index aa7a3852a7..e558ffe0fb 100644 --- a/osu.Game/Beatmaps/INamedFileUsage.cs +++ b/osu.Game/Database/INamedFileUsage.cs @@ -1,9 +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.Game.IO; + #nullable enable -namespace osu.Game.Beatmaps +namespace osu.Game.Database { /// /// A usage of a file, with a local filename attached. diff --git a/osu.Game/IO/FileInfo.cs b/osu.Game/IO/FileInfo.cs index e04bfb46cc..331546f9f8 100644 --- a/osu.Game/IO/FileInfo.cs +++ b/osu.Game/IO/FileInfo.cs @@ -6,7 +6,7 @@ using osu.Game.Database; namespace osu.Game.IO { - public class FileInfo : IHasPrimaryKey + public class FileInfo : IHasPrimaryKey, IFileInfo { public int ID { get; set; } diff --git a/osu.Game/Beatmaps/IFileInfo.cs b/osu.Game/IO/IFileInfo.cs similarity index 93% rename from osu.Game/Beatmaps/IFileInfo.cs rename to osu.Game/IO/IFileInfo.cs index 50eb223fc4..080d8e57f5 100644 --- a/osu.Game/Beatmaps/IFileInfo.cs +++ b/osu.Game/IO/IFileInfo.cs @@ -3,7 +3,7 @@ #nullable enable -namespace osu.Game.Beatmaps +namespace osu.Game.IO { /// /// A representation of a tracked file. diff --git a/osu.Game/Beatmaps/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs similarity index 96% rename from osu.Game/Beatmaps/IRulesetInfo.cs rename to osu.Game/Rulesets/IRulesetInfo.cs index b31ebdfbfd..d4dec0de64 100644 --- a/osu.Game/Beatmaps/IRulesetInfo.cs +++ b/osu.Game/Rulesets/IRulesetInfo.cs @@ -2,11 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Game.Rulesets; +using osu.Game.Database; #nullable enable -namespace osu.Game.Beatmaps +namespace osu.Game.Rulesets { /// /// A representation of a ruleset's metadata. diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 59ec9cdd7e..ca6a083a58 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -10,7 +10,7 @@ using osu.Framework.Testing; namespace osu.Game.Rulesets { [ExcludeFromDynamicCompile] - public class RulesetInfo : IEquatable + public class RulesetInfo : IEquatable, IRulesetInfo { public int? ID { get; set; } @@ -54,5 +54,11 @@ namespace osu.Game.Rulesets } public override string ToString() => Name ?? $"{Name} ({ShortName}) ID: {ID}"; + + #region Implementation of IHasOnlineID + + public int? OnlineID => ID; + + #endregion } } From 8595eb2d11d8540b01587efe5bd2ec0494639c34 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 14:38:30 +0900 Subject: [PATCH 119/391] Switch `BeatmapDifficulty` usages to use interface type --- osu.Game/Beatmaps/BeatmapDifficulty.cs | 45 +------------------ osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs | 2 +- .../Scoring/DrainingHealthProcessor.cs | 3 +- osu.Game/Rulesets/Scoring/HitWindows.cs | 4 +- 4 files changed, 7 insertions(+), 47 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 1844b193f2..0443422b31 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -2,10 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Database; +using osu.Game.Models.Interfaces; namespace osu.Game.Beatmaps { - public class BeatmapDifficulty : IHasPrimaryKey + public class BeatmapDifficulty : IHasPrimaryKey, IBeatmapDifficultyInfo { /// /// The default value used for all difficulty settings except and . @@ -49,47 +50,5 @@ namespace osu.Game.Beatmaps difficulty.SliderMultiplier = SliderMultiplier; difficulty.SliderTickRate = SliderTickRate; } - - /// - /// Maps a difficulty value [0, 10] to a two-piece linear range of values. - /// - /// The difficulty value to be mapped. - /// Minimum of the resulting range which will be achieved by a difficulty value of 0. - /// Midpoint of the resulting range which will be achieved by a difficulty value of 5. - /// Maximum of the resulting range which will be achieved by a difficulty value of 10. - /// Value to which the difficulty value maps in the specified range. - public static double DifficultyRange(double difficulty, double min, double mid, double max) - { - if (difficulty > 5) - return mid + (max - mid) * (difficulty - 5) / 5; - if (difficulty < 5) - return mid - (mid - min) * (5 - difficulty) / 5; - - return mid; - } - - /// - /// Maps a difficulty value [0, 10] to a two-piece linear range of values. - /// - /// The difficulty value to be mapped. - /// The values that define the two linear ranges. - /// - /// - /// od0 - /// Minimum of the resulting range which will be achieved by a difficulty value of 0. - /// - /// - /// od5 - /// Midpoint of the resulting range which will be achieved by a difficulty value of 5. - /// - /// - /// od10 - /// Maximum of the resulting range which will be achieved by a difficulty value of 10. - /// - /// - /// - /// Value to which the difficulty value maps in the specified range. - public static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range) - => DifficultyRange(difficulty, range.od0, range.od5, range.od10); } } diff --git a/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs index 6d9fcfcb06..339364d442 100644 --- a/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapDifficultyInfo.cs @@ -84,7 +84,7 @@ namespace osu.Game.Beatmaps /// /// /// Value to which the difficulty value maps in the specified range. - public static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range) + static double DifficultyRange(double difficulty, (double od0, double od5, double od10) range) => DifficultyRange(difficulty, range.od0, range.od5, range.od10); } } diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index cae41e22f4..785a729c6d 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Models.Interfaces; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Utils; @@ -100,7 +101,7 @@ namespace osu.Game.Rulesets.Scoring .First() ))); - targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, min_health_target, mid_health_target, max_health_target); + targetMinimumHealth = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, min_health_target, mid_health_target, max_health_target); // Add back a portion of the amount of HP to be drained, depending on the lenience requested. targetMinimumHealth += drainLenience * (1 - targetMinimumHealth); diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 410614de07..71a2af03fc 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using osu.Game.Beatmaps; +using osu.Game.Models.Interfaces; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Scoring { foreach (var range in GetRanges()) { - var value = BeatmapDifficulty.DifficultyRange(difficulty, (range.Min, range.Average, range.Max)); + var value = IBeatmapDifficultyInfo.DifficultyRange(difficulty, (range.Min, range.Average, range.Max)); switch (range.Result) { From a92d499d7a33f938cbeb91a1d174ff6d669c1cd2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 14:56:42 +0900 Subject: [PATCH 120/391] Convert usages of `BeatmapDifficulty` to `IBeatmapDifficultyInfo` --- .../TestSceneCatcher.cs | 4 ++-- .../TestSceneCatcherArea.cs | 4 ++-- .../Difficulty/CatchDifficultyCalculator.cs | 2 +- .../Edit/CatchEditorPlayfield.cs | 2 +- .../Objects/CatchHitObject.cs | 4 ++-- .../Objects/JuiceStream.cs | 2 +- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 4 ++-- osu.Game.Rulesets.Catch/UI/Catcher.cs | 6 +++--- .../UI/DrawableCatchRuleset.cs | 2 +- .../Beatmaps/ManiaBeatmapConverter.cs | 2 +- .../Patterns/Legacy/PatternGenerator.cs | 2 +- osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 2 +- .../Checks/CheckTooShortSpinnersTest.cs | 12 +++++------ .../TestSceneObjectOrderedHitPolicy.cs | 2 +- .../TestSceneStartTimeOrderedHitPolicy.cs | 2 +- .../Difficulty/OsuDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 2 +- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 4 ++-- osu.Game.Rulesets.Osu/Objects/Slider.cs | 2 +- .../Objects/SliderEndCircle.cs | 2 +- osu.Game.Rulesets.Osu/Objects/SliderTick.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 4 ++-- .../Beatmaps/TaikoBeatmapConverter.cs | 7 ++++--- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 2 +- .../Scoring/TaikoHealthProcessor.cs | 4 ++-- osu.Game.Tournament/Components/SongBar.cs | 2 +- osu.Game/Beatmaps/BeatmapDifficulty.cs | 21 ++++++++++++++++++- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 2 +- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 2 +- osu.Game/Rulesets/Mods/ModHardRock.cs | 2 +- osu.Game/Rulesets/Objects/HitObject.cs | 4 ++-- .../Rulesets/Objects/Legacy/ConvertSlider.cs | 2 +- .../Scoring/DrainingHealthProcessor.cs | 1 - osu.Game/Rulesets/Scoring/HitWindows.cs | 2 +- .../Screens/Select/Details/AdvancedStats.cs | 4 ++-- osu.Game/osu.Game.csproj | 3 +++ 36 files changed, 75 insertions(+), 53 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 540f02580f..f291bfed13 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -290,7 +290,7 @@ namespace osu.Game.Rulesets.Catch.Tests { public IEnumerable CaughtObjects => this.ChildrenOfType(); - public TestCatcher(DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty) + public TestCatcher(DroppedObjectContainer droppedObjectTarget, IBeatmapDifficultyInfo difficulty) : base(droppedObjectTarget, difficulty) { } @@ -298,7 +298,7 @@ namespace osu.Game.Rulesets.Catch.Tests public class TestKiaiFruit : Fruit { - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true }); base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 6abfbdbe21..7cae9b18b9 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Tests private ScheduledDelegate addManyFruit; - private BeatmapDifficulty beatmapDifficulty; + private IBeatmapDifficultyInfo beatmapDifficulty; public TestSceneCatcherArea() { @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Catch.Tests private class TestCatcherArea : CatcherArea { - public TestCatcherArea(BeatmapDifficulty beatmapDifficulty) + public TestCatcherArea(IBeatmapDifficultyInfo beatmapDifficulty) { var droppedObjectContainer = new DroppedObjectContainer(); Add(droppedObjectContainer); diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 82d76252d2..5b1f613f8d 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty return new CatchDifficultyAttributes { Mods = mods, Skills = skills }; // this is the same as osu!, so there's potential to share the implementation... maybe - double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; + double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; return new CatchDifficultyAttributes { diff --git a/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs index 8c9f292aa9..046ba0ebce 100644 --- a/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs +++ b/osu.Game.Rulesets.Catch/Edit/CatchEditorPlayfield.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Catch.Edit public class CatchEditorPlayfield : CatchPlayfield { // TODO fixme: the size of the catcher is not changed when circle size is changed in setup screen. - public CatchEditorPlayfield(BeatmapDifficulty difficulty) + public CatchEditorPlayfield(IBeatmapDifficultyInfo difficulty) : base(difficulty) { } diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index d43e6f1c8b..ee10cf9711 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -128,11 +128,11 @@ namespace osu.Game.Rulesets.Catch.Objects /// public int RandomSeed => (int)StartTime; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); + TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2; } diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index a8ad34fcbe..0d6925a83d 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Catch.Objects /// public double SpanDuration => Duration / this.SpanCount(); - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 1e20643a08..df32d917ce 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -34,9 +34,9 @@ namespace osu.Game.Rulesets.Catch.UI internal CatcherArea CatcherArea { get; private set; } - private readonly BeatmapDifficulty difficulty; + private readonly IBeatmapDifficultyInfo difficulty; - public CatchPlayfield(BeatmapDifficulty difficulty) + public CatchPlayfield(IBeatmapDifficultyInfo difficulty) { this.difficulty = difficulty; } diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 5cd85aac56..3745099010 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Catch.UI private readonly DrawablePool caughtBananaPool; private readonly DrawablePool caughtDropletPool; - public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, BeatmapDifficulty difficulty = null) + public Catcher([NotNull] DroppedObjectContainer droppedObjectTarget, IBeatmapDifficultyInfo difficulty = null) { this.droppedObjectTarget = droppedObjectTarget; @@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Catch.UI /// /// Calculates the scale of the catcher based off the provided beatmap difficulty. /// - private static Vector2 calculateScale(BeatmapDifficulty difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); + private static Vector2 calculateScale(IBeatmapDifficultyInfo difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); /// /// Calculates the width of the area used for attempting catches in gameplay. @@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Catch.UI /// Calculates the width of the area used for attempting catches in gameplay. /// /// The beatmap difficulty. - public static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty)); + public static float CalculateCatchWidth(IBeatmapDifficultyInfo difficulty) => CalculateCatchWidth(calculateScale(difficulty)); /// /// Determine if this catcher can catch a in the current position. diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index 8b6a074426..ba6e9224c9 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.UI : base(ruleset, beatmap, mods) { Direction.Value = ScrollingDirection.Down; - TimeRange.Value = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450); + TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450); } protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 26393c8edb..9745285b38 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) { - BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty; + IBeatmapDifficultyInfo difficulty = original.BeatmapInfo.BaseDifficulty; int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate); Random = new FastRandom(seed); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs index e643b82271..d65e78bb49 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (drainTime == 0) drainTime = 10000; - BeatmapDifficulty difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty; + IBeatmapDifficultyInfo difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty; conversionDifficulty = ((difficulty.DrainRate + Math.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15; conversionDifficulty = Math.Min(conversionDifficulty.Value, 12); diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index 43e876b7aa..c1937af7e4 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Objects /// private double tickSpacing = 50; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs index 6a3f168ee1..787807a8ea 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/Checks/CheckTooShortSpinnersTest.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks public class CheckTooShortSpinnersTest { private CheckTooShortSpinners check; - private BeatmapDifficulty difficulty; + private IBeatmapDifficultyInfo difficulty; [SetUp] public void Setup() @@ -81,12 +81,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks assertTooShort(new List { spinnerHighOd }, difficultyHighOd); } - private void assertOk(List hitObjects, BeatmapDifficulty beatmapDifficulty) + private void assertOk(List hitObjects, IBeatmapDifficultyInfo beatmapDifficulty) { Assert.That(check.Run(getContext(hitObjects, beatmapDifficulty)), Is.Empty); } - private void assertVeryShort(List hitObjects, BeatmapDifficulty beatmapDifficulty) + private void assertVeryShort(List hitObjects, IBeatmapDifficultyInfo beatmapDifficulty) { var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList(); @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateVeryShort); } - private void assertTooShort(List hitObjects, BeatmapDifficulty beatmapDifficulty) + private void assertTooShort(List hitObjects, IBeatmapDifficultyInfo beatmapDifficulty) { var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList(); @@ -102,12 +102,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateTooShort); } - private BeatmapVerifierContext getContext(List hitObjects, BeatmapDifficulty beatmapDifficulty) + private BeatmapVerifierContext getContext(List hitObjects, IBeatmapDifficultyInfo beatmapDifficulty) { var beatmap = new Beatmap { HitObjects = hitObjects, - BeatmapInfo = new BeatmapInfo { BaseDifficulty = beatmapDifficulty } + BeatmapInfo = new BeatmapInfo { BaseDifficulty = new BeatmapDifficulty(beatmapDifficulty) } }; return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap)); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index 77a68b714b..cfce80a2b2 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -452,7 +452,7 @@ namespace osu.Game.Rulesets.Osu.Tests private class TestSpinner : Spinner { - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); SpinsRequired = 1; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs index 177a4f50a1..1b85e0efde 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs @@ -412,7 +412,7 @@ namespace osu.Game.Rulesets.Osu.Tests private class TestSpinner : Spinner { - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); SpinsRequired = 1; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 4c8d0b2ce6..a8f10f44dc 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; - double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; + double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; int maxCombo = beatmap.HitObjects.Count; // Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 210d5e0403..b0c655b106 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Mods #region Reduce AR (IApplicableToDifficulty) - public void ReadFromDifficulty(BeatmapDifficulty difficulty) + public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty) { } diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 36629fa41e..7c45b2bc07 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -122,11 +122,11 @@ namespace osu.Game.Rulesets.Osu.Objects }); } - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN); + TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 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. diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index c4420b1e87..1d2666f46b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -135,7 +135,7 @@ namespace osu.Game.Rulesets.Osu.Objects Path.Version.ValueChanged += _ => updateNestedPositions(); } - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs index a6aed2c00e..f893559548 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Objects public double SpanDuration => slider.SpanDuration; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index 725dbe81fb..e7e64954e9 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Objects public int SpanIndex { get; set; } public double SpanStartTime { get; set; } - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 194aa640f9..f85dc0d391 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects /// public int MaximumBonusSpins { get; protected set; } = 1; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Objects double secondsDuration = Duration / 1000; - double minimumRotationsPerSecond = stable_matching_fudge * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); + double minimumRotationsPerSecond = stable_matching_fudge * IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond); MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration); diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 9b73e644c5..3b5b972c01 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps case IHasDuration endTimeData: { - double hitMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier; + double hitMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier; yield return new Swell { @@ -193,9 +193,10 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps private class TaikoMutliplierAppliedDifficulty : BeatmapDifficulty { - public TaikoMutliplierAppliedDifficulty(BeatmapDifficulty difficulty) + public TaikoMutliplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty) { - difficulty.CopyTo(this); + CopyFrom(difficulty); + SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index b0634295d0..0318e32991 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Taiko.Objects private float overallDifficulty = BeatmapDifficulty.DEFAULT_DIFFICULTY; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index f7a1d130eb..94cd411d7b 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Taiko.Scoring { base.ApplyBeatmap(beatmap); - hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType().Count()) * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); - hpMissMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120); + hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType().Count()) * IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); + hpMissMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120); } protected override double GetHealthIncreaseFor(JudgementResult result) diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index 6080f7b636..7bb01ddc6d 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -117,7 +117,7 @@ namespace osu.Game.Tournament.Components if ((mods & LegacyMods.DoubleTime) > 0) { // temporary local calculation (taken from OsuDifficultyCalculator) - double preempt = (int)BeatmapDifficulty.DifficultyRange(ar, 1800, 1200, 450) / 1.5; + double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(ar, 1800, 1200, 450) / 1.5; ar = (float)(preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5); bpm *= 1.5f; diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 0443422b31..2bb0787b4c 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Database; -using osu.Game.Models.Interfaces; namespace osu.Game.Beatmaps { @@ -21,6 +20,15 @@ namespace osu.Game.Beatmaps private float? approachRate; + public BeatmapDifficulty() + { + } + + public BeatmapDifficulty(IBeatmapDifficultyInfo source) + { + CopyFrom(source); + } + public float ApproachRate { get => approachRate ?? OverallDifficulty; @@ -40,6 +48,17 @@ namespace osu.Game.Beatmaps return diff; } + public void CopyFrom(IBeatmapDifficultyInfo difficulty) + { + ApproachRate = difficulty.ApproachRate; + DrainRate = difficulty.DrainRate; + CircleSize = difficulty.CircleSize; + OverallDifficulty = difficulty.OverallDifficulty; + + SliderMultiplier = difficulty.SliderMultiplier; + SliderTickRate = difficulty.SliderTickRate; + } + public void CopyTo(BeatmapDifficulty difficulty) { difficulty.ApproachRate = ApproachRate; diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 82e90399c9..b7529f39ca 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Edit } /// - /// Invokes , + /// Invokes , /// refreshing and parameters for the . /// protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty); diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index b78c30e8a5..eefa1531c4 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Mods } } - public void ReadFromDifficulty(BeatmapDifficulty difficulty) + public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty) { } diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs index 4edcb0b074..da838f9ea6 100644 --- a/osu.Game/Rulesets/Mods/ModHardRock.cs +++ b/osu.Game/Rulesets/Mods/ModHardRock.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mods public override string Description => "Everything just got a bit harder..."; public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModDifficultyAdjust) }; - public void ReadFromDifficulty(BeatmapDifficulty difficulty) + public void ReadFromDifficulty(IBeatmapDifficultyInfo difficulty) { } diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index ae0cb895bc..0b159819d4 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Objects /// The control points. /// The difficulty settings to use. /// The cancellation token. - public void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty, CancellationToken cancellationToken = default) + public void ApplyDefaults(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty, CancellationToken cancellationToken = default) { ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.Objects DefaultsApplied?.Invoke(this); } - protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected virtual void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { Kiai = controlPointInfo.EffectPointAt(StartTime + control_point_leniency).KiaiMode; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index df569b91c1..e1de82ade7 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Objects.Legacy public double Velocity = 1; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 785a729c6d..85693abb93 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; -using osu.Game.Models.Interfaces; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Utils; diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 71a2af03fc..3ffd1eb66b 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using osu.Game.Models.Interfaces; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 53e30fd9ca..a855322e57 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -106,12 +106,12 @@ namespace osu.Game.Screens.Select.Details private void updateStatistics() { - BeatmapDifficulty baseDifficulty = Beatmap?.BaseDifficulty; + IBeatmapDifficultyInfo baseDifficulty = Beatmap?.BaseDifficulty; BeatmapDifficulty adjustedDifficulty = null; if (baseDifficulty != null && mods.Value.Any(m => m is IApplicableToDifficulty)) { - adjustedDifficulty = baseDifficulty.Clone(); + adjustedDifficulty = new BeatmapDifficulty(baseDifficulty); foreach (var mod in mods.Value.OfType()) mod.ApplyToDifficulty(adjustedDifficulty); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ba118c5240..c8914353f8 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -43,4 +43,7 @@ + + + From 05996cc2e9232e7c54e1892ca2c3a253e7783513 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 17:03:08 +0900 Subject: [PATCH 121/391] Add changes that got forgotted in branch surgery --- .../Difficulty/CatchDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs | 2 +- osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 2 +- osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs | 2 +- osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs | 4 ++-- osu.Game.Tournament/Components/SongBar.cs | 2 +- osu.Game/Beatmaps/BeatmapDifficulty.cs | 1 - osu.Game/Beatmaps/BeatmapInfo.cs | 1 - osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs | 1 - osu.Game/Rulesets/Scoring/HitWindows.cs | 2 +- 13 files changed, 11 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 82d76252d2..5b1f613f8d 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty return new CatchDifficultyAttributes { Mods = mods, Skills = skills }; // this is the same as osu!, so there's potential to share the implementation... maybe - double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; + double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; return new CatchDifficultyAttributes { diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index d43e6f1c8b..32fdc9f62d 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Catch.Objects { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); + TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2; } diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index 8b6a074426..ba6e9224c9 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.UI : base(ruleset, beatmap, mods) { Direction.Value = ScrollingDirection.Down; - TimeRange.Value = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450); + TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450); } protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 4c8d0b2ce6..a8f10f44dc 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; - double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; + double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; int maxCombo = beatmap.HitObjects.Count; // Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above) diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 36629fa41e..7015c5bce2 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Objects { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN); + TimePreempt = (float)IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 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. diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 194aa640f9..d5f08f049c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Objects double secondsDuration = Duration / 1000; - double minimumRotationsPerSecond = stable_matching_fudge * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); + double minimumRotationsPerSecond = stable_matching_fudge * IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond); MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration); diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 9b73e644c5..7068e469d2 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps case IHasDuration endTimeData: { - double hitMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier; + double hitMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier; yield return new Swell { diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index f7a1d130eb..94cd411d7b 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Taiko.Scoring { base.ApplyBeatmap(beatmap); - hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType().Count()) * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); - hpMissMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120); + hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType().Count()) * IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); + hpMissMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120); } protected override double GetHealthIncreaseFor(JudgementResult result) diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index 6080f7b636..7bb01ddc6d 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -117,7 +117,7 @@ namespace osu.Game.Tournament.Components if ((mods & LegacyMods.DoubleTime) > 0) { // temporary local calculation (taken from OsuDifficultyCalculator) - double preempt = (int)BeatmapDifficulty.DifficultyRange(ar, 1800, 1200, 450) / 1.5; + double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(ar, 1800, 1200, 450) / 1.5; ar = (float)(preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5); bpm *= 1.5f; diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 0443422b31..1bb1c86c1f 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Database; -using osu.Game.Models.Interfaces; namespace osu.Game.Beatmaps { diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index d2b47ef1a4..d6f3bf0de4 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -24,7 +24,6 @@ namespace osu.Game.Beatmaps public int BeatmapVersion; private int? onlineBeatmapID; - private IRulesetInfo ruleset; [JsonProperty("id")] public int? OnlineBeatmapID diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 785a729c6d..85693abb93 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; -using osu.Game.Models.Interfaces; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Utils; diff --git a/osu.Game/Rulesets/Scoring/HitWindows.cs b/osu.Game/Rulesets/Scoring/HitWindows.cs index 71a2af03fc..3ffd1eb66b 100644 --- a/osu.Game/Rulesets/Scoring/HitWindows.cs +++ b/osu.Game/Rulesets/Scoring/HitWindows.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using osu.Game.Models.Interfaces; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring From 00e33a1da75496db67ccea0269e7f5d174b02f3d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 17:06:09 +0900 Subject: [PATCH 122/391] Fix incorrect `OnlineID` mappings --- osu.Game/Beatmaps/BeatmapInfo.cs | 2 +- osu.Game/Beatmaps/BeatmapSetInfo.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index d6f3bf0de4..1b3e67e2e7 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -190,7 +190,7 @@ namespace osu.Game.Beatmaps #region Implementation of IHasOnlineID - public int? OnlineID => ID; + public int? OnlineID => OnlineBeatmapID; #endregion diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 739acb9a8d..7e26b154a9 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -93,7 +93,7 @@ namespace osu.Game.Beatmaps #region Implementation of IHasOnlineID - public int? OnlineID => ID; + public int? OnlineID => OnlineBeatmapSetID; #endregion From 9dae92e78cf9db6caf0b1e659d17be04017630d6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 17:22:25 +0900 Subject: [PATCH 123/391] Add missing backlink to `BeatmapSet` from `Beatmap` and fix non-explicit implementations --- osu.Game/Beatmaps/BeatmapInfo.cs | 7 ++++--- osu.Game/Beatmaps/IBeatmapInfo.cs | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 1b3e67e2e7..83e547218b 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -196,11 +196,12 @@ namespace osu.Game.Beatmaps #region Implementation of IBeatmapInfo - public string DifficultyName => Version; + string IBeatmapInfo.DifficultyName => Version; IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata; - public IBeatmapDifficultyInfo Difficulty => BaseDifficulty; + IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => BaseDifficulty; + IBeatmapSetInfo IBeatmapInfo.BeatmapSet => BeatmapSet; IRulesetInfo IBeatmapInfo.Ruleset => Ruleset; - public double StarRating => StarDifficulty; + double IBeatmapInfo.StarRating => StarDifficulty; #endregion } diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs index 8ba8f316ed..fa2ce2949b 100644 --- a/osu.Game/Beatmaps/IBeatmapInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapInfo.cs @@ -29,6 +29,11 @@ namespace osu.Game.Beatmaps /// IBeatmapDifficultyInfo Difficulty { get; } + /// + /// The beatmap set this beatmap is part of. + /// + IBeatmapSetInfo BeatmapSet { get; } + /// /// The playable length in milliseconds of this beatmap. /// From d6618a99a3b885e0163043c76b011d648c5367af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 17:31:27 +0900 Subject: [PATCH 124/391] Redirect more methods to interface implementations --- osu.Game/Beatmaps/BeatmapInfo.cs | 8 ++--- osu.Game/Beatmaps/BeatmapMetadata.cs | 46 +++------------------------- osu.Game/Beatmaps/BeatmapSetInfo.cs | 2 +- 3 files changed, 8 insertions(+), 48 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 83e547218b..09f237a5de 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -157,13 +157,9 @@ namespace osu.Game.Beatmaps Version }.Concat(Metadata?.SearchableTerms ?? Enumerable.Empty()).Where(s => !string.IsNullOrEmpty(s)).ToArray(); - public override string ToString() => $"{Metadata ?? BeatmapSet?.Metadata} {versionString}".Trim(); + public override string ToString() => ((IBeatmapInfo)this).DisplayTitle; - public RomanisableString ToRomanisableString() - { - var metadata = (Metadata ?? BeatmapSet?.Metadata)?.ToRomanisableString() ?? new RomanisableString(null, null); - return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim()); - } + public RomanisableString ToRomanisableString() => ((IBeatmapInfo)this).DisplayTitleRomanisable; public bool Equals(BeatmapInfo other) { diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index fbd47d2614..3da80580cb 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; -using System.Linq; using Newtonsoft.Json; using osu.Framework.Localisation; using osu.Framework.Testing; @@ -83,51 +82,16 @@ namespace osu.Game.Beatmaps public int PreviewTime { get; set; } public string AudioFile { get; set; } + public string BackgroundFile { get; set; } - public override string ToString() - { - string author = Author == null ? string.Empty : $"({Author})"; - return $"{Artist} - {Title} {author}".Trim(); - } + public bool Equals(BeatmapMetadata other) => ((IBeatmapMetadataInfo)this).Equals(other); - public RomanisableString ToRomanisableString() - { - string author = Author == null ? string.Empty : $"({Author})"; - var artistUnicode = string.IsNullOrEmpty(ArtistUnicode) ? Artist : ArtistUnicode; - var titleUnicode = string.IsNullOrEmpty(TitleUnicode) ? Title : TitleUnicode; + public override string ToString() => ((IBeatmapMetadataInfo)this).DisplayTitle; - return new RomanisableString($"{artistUnicode} - {titleUnicode} {author}".Trim(), $"{Artist} - {Title} {author}".Trim()); - } + public RomanisableString ToRomanisableString() => ((IBeatmapMetadataInfo)this).DisplayTitleRomanisable; - [JsonIgnore] - public string[] SearchableTerms => new[] - { - Author?.Username, - Artist, - ArtistUnicode, - Title, - TitleUnicode, - Source, - Tags - }.Where(s => !string.IsNullOrEmpty(s)).ToArray(); - - public bool Equals(BeatmapMetadata other) - { - if (other == null) - return false; - - return Title == other.Title - && TitleUnicode == other.TitleUnicode - && Artist == other.Artist - && ArtistUnicode == other.ArtistUnicode - && AuthorString == other.AuthorString - && Source == other.Source - && Tags == other.Tags - && PreviewTime == other.PreviewTime - && AudioFile == other.AudioFile - && BackgroundFile == other.BackgroundFile; - } + public IEnumerable SearchableTerms => ((IBeatmapMetadataInfo)this).SearchableTerms; string IBeatmapMetadataInfo.Author => AuthorString; } diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 7e26b154a9..4804c7032c 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -61,7 +61,7 @@ namespace osu.Game.Beatmaps public string Hash { get; set; } - public string StoryboardFile => Files.Find(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename; + public string StoryboardFile => ((IBeatmapSetInfo)this).StoryboardFile; /// /// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null. From 0daf8937e3a954f0ea595e5a2c4131bdb616e0a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 17:33:57 +0900 Subject: [PATCH 125/391] Add missing xmldoc --- osu.Game/Beatmaps/IBeatmapInfo.cs | 6 ++++++ osu.Game/Beatmaps/IBeatmapMetadataInfo.cs | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs index fa2ce2949b..6153a0af08 100644 --- a/osu.Game/Beatmaps/IBeatmapInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapInfo.cs @@ -64,8 +64,14 @@ namespace osu.Game.Beatmaps /// double StarRating { get; } + /// + /// A user-presentable display title representing this metadata. + /// string DisplayTitle => $"{Metadata} {versionString}".Trim(); + /// + /// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields. + /// RomanisableString DisplayTitleRomanisable { get diff --git a/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs b/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs index 18dd38b404..d0dae296a0 100644 --- a/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs @@ -65,6 +65,9 @@ namespace osu.Game.Beatmaps /// string BackgroundFile { get; } + /// + /// A user-presentable display title representing this metadata. + /// string DisplayTitle { get @@ -74,6 +77,9 @@ namespace osu.Game.Beatmaps } } + /// + /// A user-presentable display title representing this metadata, with localisation handling for potentially romanisable fields. + /// RomanisableString DisplayTitleRomanisable { get @@ -86,6 +92,9 @@ namespace osu.Game.Beatmaps } } + /// + /// An array of all searchable terms provided in contained metadata. + /// string[] SearchableTerms => new[] { Author, From 3faafd7200576bee1016ba6c75d2643e1d7af751 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 18:24:46 +0900 Subject: [PATCH 126/391] Rename parameter to `repeatCount` and add guards --- .../Formats/LegacyStoryboardDecoder.cs | 4 ++-- osu.Game/Storyboards/CommandLoop.cs | 23 ++++++++++++++----- osu.Game/Storyboards/StoryboardSprite.cs | 10 ++++---- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 6301c42deb..5b03212da4 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -176,8 +176,8 @@ namespace osu.Game.Beatmaps.Formats case "L": { var startTime = Parsing.ParseDouble(split[1]); - var loopCount = Parsing.ParseInt(split[2]); - timelineGroup = storyboardSprite?.AddLoop(startTime, loopCount); + var repeatCount = Parsing.ParseInt(split[2]); + timelineGroup = storyboardSprite?.AddLoop(startTime, Math.Max(0, repeatCount)); break; } diff --git a/osu.Game/Storyboards/CommandLoop.cs b/osu.Game/Storyboards/CommandLoop.cs index c17436d813..66db965803 100644 --- a/osu.Game/Storyboards/CommandLoop.cs +++ b/osu.Game/Storyboards/CommandLoop.cs @@ -9,20 +9,31 @@ namespace osu.Game.Storyboards public class CommandLoop : CommandTimelineGroup { public double LoopStartTime; - public int LoopCount; + + /// + /// 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 => StartTime + CommandsDuration * LoopCount; + public override double EndTime => StartTime + CommandsDuration * TotalIterations; - public CommandLoop(double startTime, int loopCount) + /// + /// 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; - LoopCount = Math.Max(1, loopCount); + TotalIterations = repeatCount + 1; } public override IEnumerable.TypedCommand> GetCommands(CommandTimelineSelector timelineSelector, double offset = 0) { - for (var loop = 0; loop < LoopCount; loop++) + for (var loop = 0; loop < TotalIterations; loop++) { var loopOffset = LoopStartTime + loop * CommandsDuration; foreach (var command in base.GetCommands(timelineSelector, offset + loopOffset)) @@ -31,6 +42,6 @@ namespace osu.Game.Storyboards } public override string ToString() - => $"{LoopStartTime} x{LoopCount}"; + => $"{LoopStartTime} x{TotalIterations}"; } } diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index bf87e7d10e..6fb2f5994b 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.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. -using osuTK; -using osu.Framework.Graphics; -using osu.Game.Storyboards.Drawables; using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; +using osu.Framework.Graphics; +using osu.Game.Storyboards.Drawables; +using osuTK; namespace osu.Game.Storyboards { @@ -78,9 +78,9 @@ namespace osu.Game.Storyboards InitialPosition = initialPosition; } - public CommandLoop AddLoop(double startTime, int loopCount) + public CommandLoop AddLoop(double startTime, int repeatCount) { - var loop = new CommandLoop(startTime, loopCount); + var loop = new CommandLoop(startTime, repeatCount); loops.Add(loop); return loop; } From 4c28749d7310a658e9a8329f39064afb03306311 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 19:05:08 +0900 Subject: [PATCH 127/391] Fix incorrect legacy decoder usage --- osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 5b03212da4..0f15e28c00 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -177,7 +177,7 @@ namespace osu.Game.Beatmaps.Formats { var startTime = Parsing.ParseDouble(split[1]); var repeatCount = Parsing.ParseInt(split[2]); - timelineGroup = storyboardSprite?.AddLoop(startTime, Math.Max(0, repeatCount)); + timelineGroup = storyboardSprite?.AddLoop(startTime, Math.Max(0, repeatCount - 1)); break; } From 5820a7165268fcd9776e9fd1b33c27648884349d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 1 Oct 2021 19:57:45 +0900 Subject: [PATCH 128/391] Fix mania difficulty calculator crashing --- .../Difficulty/ManiaDifficultyCalculator.cs | 8 ++-- .../Difficulty/DifficultyCalculator.cs | 38 +++++++++++-------- .../HUD/DefaultPerformancePointsCounter.cs | 4 +- 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index a7a6677b68..fc29eadedc 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty Mods = mods, // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate), - ScoreMultiplier = getScoreMultiplier(beatmap, mods), + ScoreMultiplier = getScoreMultiplier(mods), MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1), Skills = skills }; @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[] { - new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns) + new Strain(mods, ((ManiaBeatmap)Beatmap).TotalColumns) }; protected override Mod[] DifficultyAdjustmentMods @@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty } } - private double getScoreMultiplier(IBeatmap beatmap, Mod[] mods) + private double getScoreMultiplier(Mod[] mods) { double scoreMultiplier = 1; @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty } } - var maniaBeatmap = (ManiaBeatmap)beatmap; + var maniaBeatmap = (ManiaBeatmap)Beatmap; int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns; if (diff > 0) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index f38949d982..780c2ad491 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -18,13 +18,17 @@ namespace osu.Game.Rulesets.Difficulty { public abstract class DifficultyCalculator { - private readonly Ruleset ruleset; - private readonly WorkingBeatmap beatmap; + /// + /// The beatmap for which difficulty will be calculated. + /// + protected IBeatmap Beatmap { get; private set; } - private IBeatmap playableBeatmap; private Mod[] playableMods; private double clockRate; + private readonly Ruleset ruleset; + private readonly WorkingBeatmap beatmap; + protected DifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) { this.ruleset = ruleset; @@ -40,10 +44,10 @@ namespace osu.Game.Rulesets.Difficulty { preProcess(mods); - var skills = CreateSkills(playableBeatmap, playableMods, clockRate); + var skills = CreateSkills(Beatmap, playableMods, clockRate); - if (!playableBeatmap.HitObjects.Any()) - return CreateDifficultyAttributes(playableBeatmap, playableMods, skills, clockRate); + if (!Beatmap.HitObjects.Any()) + return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate); foreach (var hitObject in getDifficultyHitObjects()) { @@ -51,18 +55,18 @@ namespace osu.Game.Rulesets.Difficulty skill.ProcessInternal(hitObject); } - return CreateDifficultyAttributes(playableBeatmap, playableMods, skills, clockRate); + return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate); } public IEnumerable CalculateTimed(params Mod[] mods) { preProcess(mods); - if (!playableBeatmap.HitObjects.Any()) + if (!Beatmap.HitObjects.Any()) yield break; - var skills = CreateSkills(playableBeatmap, playableMods, clockRate); - var progressiveBeatmap = new ProgressiveCalculationBeatmap(playableBeatmap); + var skills = CreateSkills(Beatmap, playableMods, clockRate); + var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap); foreach (var hitObject in getDifficultyHitObjects()) { @@ -93,7 +97,7 @@ namespace osu.Game.Rulesets.Difficulty /// /// Retrieves the s to calculate against. /// - private IEnumerable getDifficultyHitObjects() => SortObjects(CreateDifficultyHitObjects(playableBeatmap, clockRate)); + private IEnumerable getDifficultyHitObjects() => SortObjects(CreateDifficultyHitObjects(Beatmap, clockRate)); /// /// Performs required tasks before every calculation. @@ -102,7 +106,7 @@ namespace osu.Game.Rulesets.Difficulty private void preProcess(Mod[] mods) { playableMods = mods.Select(m => m.DeepClone()).ToArray(); - playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods); + Beatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods); var track = new TrackVirtual(10000); mods.OfType().ForEach(m => m.ApplyToTrack(track)); @@ -118,7 +122,7 @@ namespace osu.Game.Rulesets.Difficulty => input.OrderBy(h => h.BaseObject.StartTime); /// - /// Creates all combinations which adjust the difficulty. + /// Creates all combinations which adjust the difficulty. /// public Mod[] CreateDifficultyAdjustmentModCombinations() { @@ -186,14 +190,15 @@ namespace osu.Game.Rulesets.Difficulty } /// - /// Retrieves all s which adjust the difficulty. + /// Retrieves all s which adjust the difficulty. /// protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty(); /// /// Creates to describe beatmap's calculated difficulty. /// - /// The whose difficulty was calculated. + /// The whose difficulty was calculated. + /// This may differ from in the case of timed calculation. /// The s that difficulty was calculated with. /// The skills which processed the beatmap. /// The rate at which the gameplay clock is run at. @@ -210,7 +215,8 @@ namespace osu.Game.Rulesets.Difficulty /// /// Creates the s to calculate the difficulty of an . /// - /// The whose difficulty will be calculated. + /// The whose difficulty will be calculated. + /// This may differ from in the case of timed calculation. /// Mods to calculate difficulty with. /// Clockrate to calculate difficulty with. /// The s. diff --git a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs index 563032b4ea..ff0c628aa6 100644 --- a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs @@ -143,9 +143,9 @@ namespace osu.Game.Screens.Play.HUD } public override IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null) - => gameplayBeatmap; + => gameplayBeatmap.PlayableBeatmap; - protected override IBeatmap GetBeatmap() => gameplayBeatmap; + protected override IBeatmap GetBeatmap() => gameplayBeatmap.PlayableBeatmap; protected override Texture GetBackground() => throw new NotImplementedException(); From 98badd644ff6b46d26500f48b125c65b2100410a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 1 Oct 2021 20:09:39 +0900 Subject: [PATCH 129/391] Add xmldocs --- osu.Game/Graphics/UserInterface/RollingCounter.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index f03287e7de..67b0b6a06b 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -160,8 +160,15 @@ namespace osu.Game.Graphics.UserInterface this.TransformTo(nameof(DisplayedCount), newValue, rollingTotalDuration, RollingEasing); } + /// + /// Creates the text. Delegates to by default. + /// protected virtual IHasText CreateText() => CreateSpriteText(); + /// + /// Creates an which may be used to display this counter's text. + /// May not be called if is overridden. + /// protected virtual OsuSpriteText CreateSpriteText() => new OsuSpriteText { Font = OsuFont.Numeric.With(size: 40f), From d0081908c5af3f22aa3ef532b982e94390704910 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 1 Oct 2021 20:52:48 +0900 Subject: [PATCH 130/391] Make Score internal --- 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 4018650093..6b1186c5ce 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -137,7 +137,7 @@ namespace osu.Game.Screens.Play public readonly PlayerConfiguration Configuration; - public Score Score { get; private set; } + internal Score Score { get; private set; } /// /// Create a new player instance. From a1f880a36aede7bb087a9c25c0e618b17813e39a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 1 Oct 2021 20:56:03 +0900 Subject: [PATCH 131/391] Split classes --- .../Difficulty/DifficultyCalculator.cs | 17 +++---------- .../Difficulty/TimedDifficultyAttributes.cs | 25 +++++++++++++++++++ .../HUD/DefaultPerformancePointsCounter.cs | 4 +-- 3 files changed, 30 insertions(+), 16 deletions(-) create mode 100644 osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 780c2ad491..3d90cc59f4 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -222,20 +222,9 @@ namespace osu.Game.Rulesets.Difficulty /// The s. protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate); - public class TimedDifficultyAttributes : IComparable - { - public readonly double Time; - public readonly DifficultyAttributes Attributes; - - public TimedDifficultyAttributes(double time, DifficultyAttributes attributes) - { - Time = time; - Attributes = attributes; - } - - public int CompareTo(TimedDifficultyAttributes other) => Time.CompareTo(other.Time); - } - + /// + /// Used to calculate timed difficulty attributes, where only a subset of hitobjects should be visible at any point in time. + /// private class ProgressiveCalculationBeatmap : IBeatmap { private readonly IBeatmap baseBeatmap; diff --git a/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.cs new file mode 100644 index 0000000000..973b2dacb2 --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/TimedDifficultyAttributes.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 System; + +namespace osu.Game.Rulesets.Difficulty +{ + /// + /// Wraps a object and adds a time value for which the attribute is valid. + /// Output by . + /// + public class TimedDifficultyAttributes : IComparable + { + public readonly double Time; + public readonly DifficultyAttributes Attributes; + + public TimedDifficultyAttributes(double time, DifficultyAttributes attributes) + { + Time = time; + Attributes = attributes; + } + + public int CompareTo(TimedDifficultyAttributes other) => Time.CompareTo(other.Time); + } +} diff --git a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs index ff0c628aa6..2bc9f78548 100644 --- a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Play.HUD [Resolved(CanBeNull = true)] private Player player { get; set; } - private DifficultyCalculator.TimedDifficultyAttributes[] timedAttributes; + private TimedDifficultyAttributes[] timedAttributes; private Ruleset gameplayRuleset; public DefaultPerformancePointsCounter() @@ -73,7 +73,7 @@ namespace osu.Game.Screens.Play.HUD if (player == null) return; - var attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new DifficultyCalculator.TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); + var attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); if (attribIndex < 0) attribIndex = ~attribIndex - 1; attribIndex = Math.Clamp(attribIndex, 0, timedAttributes.Length - 1); From 0ee148b53f4bf72441f879950be6b76d610d9c8a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 1 Oct 2021 21:31:26 +0900 Subject: [PATCH 132/391] Extra guard against no attributes --- osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs index 2bc9f78548..d93d626c72 100644 --- a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Play.HUD private void onNewJudgement(JudgementResult judgement) { - if (player == null) + if (player == null || timedAttributes.Length == 0) return; var attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); From adff418fd26c7abdff11663eb49389ec9b400268 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 1 Oct 2021 22:15:10 +0900 Subject: [PATCH 133/391] Guard against exception in skin deserialisation --- osu.Game/Skinning/Skin.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index b6cb8fc7a4..92441f40da 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; +using osu.Framework.Logging; using osu.Game.Audio; using osu.Game.IO; using osu.Game.Screens.Play.HUD; @@ -55,13 +56,20 @@ namespace osu.Game.Skinning if (bytes == null) continue; - string jsonContent = Encoding.UTF8.GetString(bytes); - var deserializedContent = JsonConvert.DeserializeObject>(jsonContent); + try + { + string jsonContent = Encoding.UTF8.GetString(bytes); + var deserializedContent = JsonConvert.DeserializeObject>(jsonContent); - if (deserializedContent == null) - continue; + if (deserializedContent == null) + continue; - DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray(); + DrawableComponentInfo[skinnableTarget] = deserializedContent.ToArray(); + } + catch (Exception ex) + { + Logger.Error(ex, "Failed to load skin configuration."); + } } } From a32f5d44e279f617ad2933b28f26c0d0250882d6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 22:23:51 +0900 Subject: [PATCH 134/391] Improve clarity of xmldoc Co-authored-by: Dan Balasescu --- osu.Game/Database/IRealmFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/IRealmFactory.cs b/osu.Game/Database/IRealmFactory.cs index 3b206d80eb..a957424584 100644 --- a/osu.Game/Database/IRealmFactory.cs +++ b/osu.Game/Database/IRealmFactory.cs @@ -13,7 +13,7 @@ namespace osu.Game.Database Realm Context { get; } /// - /// Create a new realm context for use on an arbitrary thread. + /// Create a new realm context for use on the current thread. /// Realm CreateContext(); } From 4e3d9da22d4422f2eb3b3e1f0c513ac95af45d4a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 1 Oct 2021 22:58:40 +0900 Subject: [PATCH 135/391] Increase test lenience --- .../Gameplay/TestSceneBeatmapSkinFallbacks.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index e560c81fb2..7d4673c901 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -84,18 +84,18 @@ namespace osu.Game.Tests.Visual.Gameplay Remove(expectedComponentsAdjustmentContainer); return almostEqual(actualInfo, expectedInfo); - - static bool almostEqual(SkinnableInfo info, SkinnableInfo other) => - other != null - && info.Type == other.Type - && info.Anchor == other.Anchor - && info.Origin == other.Origin - && Precision.AlmostEquals(info.Position, other.Position) - && Precision.AlmostEquals(info.Scale, other.Scale) - && Precision.AlmostEquals(info.Rotation, other.Rotation) - && info.Children.SequenceEqual(other.Children, new FuncEqualityComparer(almostEqual)); } + private static bool almostEqual(SkinnableInfo info, SkinnableInfo other) => + other != null + && info.Type == other.Type + && info.Anchor == other.Anchor + && info.Origin == other.Origin + && Precision.AlmostEquals(info.Position, other.Position, 1) + && Precision.AlmostEquals(info.Scale, other.Scale) + && Precision.AlmostEquals(info.Rotation, other.Rotation) + && info.Children.SequenceEqual(other.Children, new FuncEqualityComparer(almostEqual)); + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new CustomSkinWorkingBeatmap(beatmap, storyboard, Clock, Audio, currentBeatmapSkin); From 7884dbbd11225241c59b6a5d3864bc2dbd429460 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Fri, 1 Oct 2021 15:04:44 +0100 Subject: [PATCH 136/391] adjust formulae --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 621fb4588e..31d2c01d2e 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -110,7 +110,9 @@ 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. if (mods.Any(m => m is OsuModBlinds)) - aimValue *= 1.0 + blindsMultiplier; + aimValue *= 1.0 + (0.14 + totalHits * (0.0016 / (1 + 2 * countMiss)) + * Math.Pow(accuracy, 16)) + * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate); else if (mods.Any(h => h is OsuModHidden)) aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); @@ -234,6 +236,5 @@ namespace osu.Game.Rulesets.Osu.Difficulty private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalSuccessfulHits => countGreat + countOk + countMeh; - private double blindsMultiplier => (0.12 + totalHits * (0.0008 / (1 + 2 * countMiss)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate); } } From 57b9a91cba80049937ce6b2391e404144945a39c Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Fri, 1 Oct 2021 15:26:59 +0100 Subject: [PATCH 137/391] trim whitespace cuz code quality --- 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 31d2c01d2e..3144418979 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (mods.Any(m => m is OsuModSpunOut)) multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85); - + double aimValue = computeAimValue(); double speedValue = computeSpeedValue(); double accuracyValue = computeAccuracyValue(); From 310bf3e580a513dafee83f12e61347a61e8fe541 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Fri, 1 Oct 2021 15:29:20 +0100 Subject: [PATCH 138/391] more code quality (oops) --- 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 3144418979..d0ef0e67da 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (mods.Any(m => m is OsuModSpunOut)) multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85); - + double aimValue = computeAimValue(); double speedValue = computeSpeedValue(); double accuracyValue = computeAccuracyValue(); From 2a4a376b87ef04d7eb25e689159570e42acbd422 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Sat, 2 Oct 2021 01:21:55 +0900 Subject: [PATCH 139/391] Refactor Filter to behave closer to a Transformable --- .../Visual/Audio/TestSceneFilter.cs | 108 ++++++++++-------- osu.Game/Audio/Effects/Filter.cs | 97 ++++++++++------ .../Audio/Effects/ITransformableFilter.cs | 54 +++++++++ osu.Game/Overlays/DialogOverlay.cs | 9 +- 4 files changed, 175 insertions(+), 93 deletions(-) create mode 100644 osu.Game/Audio/Effects/ITransformableFilter.cs diff --git a/osu.Game.Tests/Visual/Audio/TestSceneFilter.cs b/osu.Game.Tests/Visual/Audio/TestSceneFilter.cs index 6a7a404d8a..79d8f7da8c 100644 --- a/osu.Game.Tests/Visual/Audio/TestSceneFilter.cs +++ b/osu.Game.Tests/Visual/Audio/TestSceneFilter.cs @@ -6,74 +6,77 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Game.Audio.Effects; using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Tests.Visual.Audio { public class TestSceneFilter : OsuTestScene { - [Resolved] - private AudioManager audio { get; set; } - private WorkingBeatmap testBeatmap; - private Filter lowPassFilter; - private Filter highPassFilter; - private Filter bandPassFilter; + + private OsuSpriteText lowpassText; + private OsuSpriteText highpassText; + private Filter lowpassFilter; + private Filter highpassFilter; [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { testBeatmap = new WaveformTestBeatmap(audio); - AddRange(new Drawable[] + lowpassFilter = new Filter(audio.TrackMixer); + highpassFilter = new Filter(audio.TrackMixer, BQFType.HighPass); + Add(new FillFlowContainer { - lowPassFilter = new Filter(audio.TrackMixer) + Children = new Drawable[] { - FilterType = BQFType.LowPass, - SweepCutoffStart = 2000, - SweepCutoffEnd = 150, - SweepDuration = 1000 - }, - highPassFilter = new Filter(audio.TrackMixer) - { - FilterType = BQFType.HighPass, - SweepCutoffStart = 150, - SweepCutoffEnd = 2000, - SweepDuration = 1000 - }, - bandPassFilter = new Filter(audio.TrackMixer) - { - FilterType = BQFType.BandPass, - SweepCutoffStart = 150, - SweepCutoffEnd = 20000, - SweepDuration = 1000 - }, + lowpassText = new OsuSpriteText + { + Padding = new MarginPadding(20), + Text = $"Low Pass: {lowpassFilter.Cutoff.Value}hz", + Font = new FontUsage(size: 40) + }, + new OsuSliderBar + { + Width = 500, + Height = 50, + Padding = new MarginPadding(20), + Current = { BindTarget = lowpassFilter.Cutoff } + }, + highpassText = new OsuSpriteText + { + Padding = new MarginPadding(20), + Text = $"High Pass: {highpassFilter.Cutoff.Value}hz", + Font = new FontUsage(size: 40) + }, + new OsuSliderBar + { + Width = 500, + Height = 50, + Padding = new MarginPadding(20), + Current = { BindTarget = highpassFilter.Cutoff } + } + } }); + lowpassFilter.Cutoff.ValueChanged += e => lowpassText.Text = $"Low Pass: {e.NewValue}hz"; + highpassFilter.Cutoff.ValueChanged += e => highpassText.Text = $"High Pass: {e.NewValue}hz"; } [Test] - public void TestLowPass() - { - testFilter(lowPassFilter); - } + public void TestLowPass() => testFilter(lowpassFilter, lowpassFilter.MaxCutoff, 0); [Test] - public void TestHighPass() - { - testFilter(highPassFilter); - } + public void TestHighPass() => testFilter(highpassFilter, 0, highpassFilter.MaxCutoff); - [Test] - public void TestBandPass() - { - testFilter(bandPassFilter); - } - - private void testFilter(Filter filter) + private void testFilter(Filter filter, int cutoffFrom, int cutoffTo) { + Add(filter); AddStep("Prepare Track", () => { - testBeatmap = new WaveformTestBeatmap(audio); testBeatmap.LoadTrack(); }); AddStep("Play Track", () => @@ -81,14 +84,19 @@ namespace osu.Game.Tests.Visual.Audio testBeatmap.Track.Start(); }); AddWaitStep("Let track play", 10); - AddStep("Enable Filter", filter.Enable); - AddWaitStep("Let track play", 10); - AddStep("Disable Filter", filter.Disable); - AddWaitStep("Let track play", 10); - AddStep("Stop Track", () => + AddStep("Filter Sweep", () => { - testBeatmap.Track.Stop(); + filter.CutoffTo(cutoffFrom).Then() + .CutoffTo(cutoffTo, 2000, cutoffFrom > cutoffTo ? Easing.OutCubic : Easing.InCubic); }); + AddWaitStep("Let track play", 10); + AddStep("Filter Sweep (reverse)", () => + { + filter.CutoffTo(cutoffTo).Then() + .CutoffTo(cutoffFrom, 2000, cutoffTo > cutoffFrom ? Easing.OutCubic : Easing.InCubic); + }); + AddWaitStep("Let track play", 10); + AddStep("Stop track", () => testBeatmap.Track.Stop()); } } } diff --git a/osu.Game/Audio/Effects/Filter.cs b/osu.Game/Audio/Effects/Filter.cs index 04ad862879..142e6b8fff 100644 --- a/osu.Game/Audio/Effects/Filter.cs +++ b/osu.Game/Audio/Effects/Filter.cs @@ -8,66 +8,87 @@ using osu.Framework.Graphics; namespace osu.Game.Audio.Effects { - public class Filter : Component + public class Filter : Component, ITransformableFilter { - public BQFType FilterType = BQFType.LowPass; - public float SweepCutoffStart = 2000; - public float SweepCutoffEnd = 150; - public float SweepDuration = 100; - public Easing SweepEasing = Easing.None; - - public bool IsActive { get; private set; } - - private readonly Bindable filterFreq = new Bindable(); + public readonly int MaxCutoff; private readonly AudioMixer mixer; - private BQFParameters filter; + private readonly BQFParameters filter; + private readonly BQFType type; + + public BindableNumber Cutoff { get; } /// /// A BiQuad filter that performs a filter-sweep when toggled on or off. /// /// The mixer this effect should be attached to. - public Filter(AudioMixer mixer) + /// The type of filter (e.g. LowPass, HighPass, etc) + public Filter(AudioMixer mixer, BQFType type = BQFType.LowPass) { this.mixer = mixer; - } + this.type = type; - public void Enable() - { - attachFilter(); - this.TransformBindableTo(filterFreq, SweepCutoffEnd, SweepDuration, SweepEasing); - } + var initialCutoff = 1; - public void Disable() - { - this.TransformBindableTo(filterFreq, SweepCutoffStart, SweepDuration, SweepEasing).OnComplete(_ => detachFilter()); - } + // These max cutoff values are a work-around for BASS' BiQuad filters behaving weirdly when approaching nyquist. + // Note that these values assume a sample rate of 44100 (as per BassAudioMixer in osu.Framework) + // See also https://www.un4seen.com/forum/?topic=19542.0 for more information. + switch (type) + { + case BQFType.HighPass: + MaxCutoff = 21968; // beyond this value, the high-pass cuts out + break; - private void attachFilter() - { - if (IsActive) return; + case BQFType.LowPass: + MaxCutoff = initialCutoff = 14000; // beyond (roughly) this value, the low-pass filter audibly wraps/reflects + break; + case BQFType.BandPass: + MaxCutoff = 16000; // beyond (roughly) this value, the band-pass filter audibly wraps/reflects + break; + + default: + MaxCutoff = 22050; // default to nyquist for other filter types, TODO: handle quirks of other filter types + break; + } + + Cutoff = new BindableNumber + { + MinValue = 1, + MaxValue = MaxCutoff + }; filter = new BQFParameters { - lFilter = FilterType, - fCenter = filterFreq.Value = SweepCutoffStart + lFilter = type, + fCenter = initialCutoff }; - mixer.Effects.Add(filter); - filterFreq.ValueChanged += updateFilter; - IsActive = true; + attachFilter(); + + Cutoff.ValueChanged += updateFilter; + Cutoff.Value = initialCutoff; } - private void detachFilter() - { - if (!IsActive) return; + private void attachFilter() => mixer.Effects.Add(filter); - filterFreq.ValueChanged -= updateFilter; - mixer.Effects.Remove(filter); - IsActive = false; - } + private void detachFilter() => mixer.Effects.Remove(filter); - private void updateFilter(ValueChangedEvent cutoff) + private void updateFilter(ValueChangedEvent cutoff) { + // This is another workaround for quirks in BASS' BiQuad filters. + // Because the cutoff can't be set above ~14khz (i.e. outside of human hearing range) without the aforementioned wrapping/reflecting quirk occuring, we instead + // remove the effect from the mixer when the cutoff is at maximum so that a LowPass filter isn't always attenuating high frequencies just by existing. + if (type == BQFType.LowPass) + { + if (cutoff.NewValue >= MaxCutoff) + { + detachFilter(); + return; + } + + if (cutoff.OldValue >= MaxCutoff && cutoff.NewValue < MaxCutoff) + attachFilter(); + } + var filterIndex = mixer.Effects.IndexOf(filter); if (filterIndex < 0) return; diff --git a/osu.Game/Audio/Effects/ITransformableFilter.cs b/osu.Game/Audio/Effects/ITransformableFilter.cs new file mode 100644 index 0000000000..e4de4cf8ff --- /dev/null +++ b/osu.Game/Audio/Effects/ITransformableFilter.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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Transforms; + +namespace osu.Game.Audio.Effects +{ + public interface ITransformableFilter + { + /// + /// The filter cutoff. + /// + BindableNumber Cutoff { get; } + } + + public static class FilterableAudioComponentExtensions + { + /// + /// Smoothly adjusts filter cutoff over time. + /// + /// A to which further transforms can be added. + public static TransformSequence CutoffTo(this T component, int newCutoff, double duration = 0, Easing easing = Easing.None) + where T : class, ITransformableFilter, IDrawable + => component.CutoffTo(newCutoff, duration, new DefaultEasingFunction(easing)); + + /// + /// Smoothly adjusts filter cutoff over time. + /// + /// A to which further transforms can be added. + public static TransformSequence CutoffTo(this TransformSequence sequence, int newCutoff, double duration = 0, Easing easing = Easing.None) + where T : class, ITransformableFilter, IDrawable + => sequence.CutoffTo(newCutoff, duration, new DefaultEasingFunction(easing)); + + /// + /// Smoothly adjusts filter cutoff over time. + /// + /// A to which further transforms can be added. + public static TransformSequence CutoffTo(this T component, int newCutoff, double duration, TEasing easing) + where T : class, ITransformableFilter, IDrawable + where TEasing : IEasingFunction + => component.TransformBindableTo(component.Cutoff, newCutoff, duration, easing); + + /// + /// Smoothly adjusts filter cutoff over time. + /// + /// A to which further transforms can be added. + public static TransformSequence CutoffTo(this TransformSequence sequence, int newCutoff, double duration, TEasing easing) + where T : class, ITransformableFilter, IDrawable + where TEasing : IEasingFunction + => sequence.Append(o => o.TransformBindableTo(o.Cutoff, newCutoff, duration, easing)); + } +} diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 8a274968b7..6016d16d29 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays protected override string PopInSampleName => "UI/dialog-pop-in"; protected override string PopOutSampleName => "UI/dialog-pop-out"; - private Filter filter; + private Filter lpFilter; public PopupDialog CurrentDialog { get; private set; } @@ -42,7 +42,7 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load(AudioManager audio) { - AddInternal(filter = new Filter(audio.TrackMixer)); + AddInternal(lpFilter = new Filter(audio.TrackMixer)); } public void Push(PopupDialog dialog) @@ -82,15 +82,13 @@ namespace osu.Game.Overlays { base.PopIn(); this.FadeIn(PopupDialog.ENTER_DURATION, Easing.OutQuint); - filter.Enable(); + lpFilter.CutoffTo(2000).Then().CutoffTo(150, 100, Easing.OutCubic); } protected override void PopOut() { base.PopOut(); - filter.Disable(); - if (CurrentDialog?.State.Value == Visibility.Visible) { CurrentDialog.Hide(); @@ -98,6 +96,7 @@ namespace osu.Game.Overlays } this.FadeOut(PopupDialog.EXIT_DURATION, Easing.InSine); + lpFilter.CutoffTo(2000, 100, Easing.InCubic).Then().CutoffTo(lpFilter.MaxCutoff); } public override bool OnPressed(KeyBindingPressEvent e) From 1eb67dc5941322db6b8b9ab5d8bdf87f10f1b579 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Oct 2021 17:01:28 +0000 Subject: [PATCH 140/391] Bump Microsoft.AspNetCore.SignalR.Client from 5.0.9 to 5.0.10 Bumps [Microsoft.AspNetCore.SignalR.Client](https://github.com/dotnet/aspnetcore) from 5.0.9 to 5.0.10. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Commits](https://github.com/dotnet/aspnetcore/compare/v5.0.9...v5.0.10) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.SignalR.Client dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 ba118c5240..9087e77a46 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -23,7 +23,7 @@ - + From 323a9a748dae5f5c133be297f6f4ae33c5cbe07b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Oct 2021 17:01:32 +0000 Subject: [PATCH 141/391] Bump HtmlAgilityPack from 1.11.36 to 1.11.37 Bumps [HtmlAgilityPack](https://github.com/zzzprojects/html-agility-pack) from 1.11.36 to 1.11.37. - [Release notes](https://github.com/zzzprojects/html-agility-pack/releases) - [Commits](https://github.com/zzzprojects/html-agility-pack/compare/v1.11.36...v1.11.37) --- updated-dependencies: - dependency-name: HtmlAgilityPack dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 ba118c5240..850cce2d29 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -20,7 +20,7 @@ - + From 6de4e981ddb4d4fb7ef2cd6546a4b518312498f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Oct 2021 17:01:37 +0000 Subject: [PATCH 142/391] Bump Realm from 10.5.0 to 10.6.0 Bumps [Realm](https://github.com/realm/realm-dotnet) from 10.5.0 to 10.6.0. - [Release notes](https://github.com/realm/realm-dotnet/releases) - [Changelog](https://github.com/realm/realm-dotnet/blob/master/CHANGELOG.md) - [Commits](https://github.com/realm/realm-dotnet/compare/10.5.0...10.6.0) --- updated-dependencies: - dependency-name: Realm dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- 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 8fad10d247..b84f1730ac 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -56,6 +56,6 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ba118c5240..b1654655a2 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -35,7 +35,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 37931d0c38..8597a06c03 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -99,6 +99,6 @@ - + From 05ca3aec4f7ed7f5ce56ab7a0e414bbcfb4d7afa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 2 Oct 2021 02:08:56 +0900 Subject: [PATCH 143/391] Rename `GameplayState` to `SpectatorGameplayState` --- .../Spectate/MultiSpectatorScreen.cs | 4 ++-- osu.Game/Screens/Play/SoloSpectator.cs | 22 +++++++++---------- ...playState.cs => SpectatorGameplayState.cs} | 6 ++--- osu.Game/Screens/Spectate/SpectatorScreen.cs | 8 +++---- 4 files changed, 20 insertions(+), 20 deletions(-) rename osu.Game/Screens/Spectate/{GameplayState.cs => SpectatorGameplayState.cs} (81%) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs index c45e3a79da..7bf8ce0e1a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/MultiSpectatorScreen.cs @@ -213,8 +213,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate { } - protected override void StartGameplay(int userId, GameplayState gameplayState) - => instances.Single(i => i.UserId == userId).LoadScore(gameplayState.Score); + protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) + => instances.Single(i => i.UserId == userId).LoadScore(spectatorGameplayState.Score); protected override void EndGameplay(int userId) { diff --git a/osu.Game/Screens/Play/SoloSpectator.cs b/osu.Game/Screens/Play/SoloSpectator.cs index 4520e2e825..9d4dad8bdc 100644 --- a/osu.Game/Screens/Play/SoloSpectator.cs +++ b/osu.Game/Screens/Play/SoloSpectator.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.Play /// The player's immediate online gameplay state. /// This doesn't always reflect the gameplay state being watched. /// - private GameplayState immediateGameplayState; + private SpectatorGameplayState immediateSpectatorGameplayState; private GetBeatmapSetRequest onlineBeatmapRequest; @@ -146,7 +146,7 @@ namespace osu.Game.Screens.Play Width = 250, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Action = () => scheduleStart(immediateGameplayState), + Action = () => scheduleStart(immediateSpectatorGameplayState), Enabled = { Value = false } } } @@ -167,18 +167,18 @@ namespace osu.Game.Screens.Play showBeatmapPanel(spectatorState); } - protected override void StartGameplay(int userId, GameplayState gameplayState) + protected override void StartGameplay(int userId, SpectatorGameplayState spectatorGameplayState) { - immediateGameplayState = gameplayState; + immediateSpectatorGameplayState = spectatorGameplayState; watchButton.Enabled.Value = true; - scheduleStart(gameplayState); + scheduleStart(spectatorGameplayState); } protected override void EndGameplay(int userId) { scheduledStart?.Cancel(); - immediateGameplayState = null; + immediateSpectatorGameplayState = null; watchButton.Enabled.Value = false; clearDisplay(); @@ -194,7 +194,7 @@ namespace osu.Game.Screens.Play private ScheduledDelegate scheduledStart; - private void scheduleStart(GameplayState gameplayState) + private void scheduleStart(SpectatorGameplayState spectatorGameplayState) { // This function may be called multiple times in quick succession once the screen becomes current again. scheduledStart?.Cancel(); @@ -203,15 +203,15 @@ namespace osu.Game.Screens.Play if (this.IsCurrentScreen()) start(); else - scheduleStart(gameplayState); + scheduleStart(spectatorGameplayState); }); void start() { - Beatmap.Value = gameplayState.Beatmap; - Ruleset.Value = gameplayState.Ruleset.RulesetInfo; + Beatmap.Value = spectatorGameplayState.Beatmap; + Ruleset.Value = spectatorGameplayState.Ruleset.RulesetInfo; - this.Push(new SpectatorPlayerLoader(gameplayState.Score, () => new SoloSpectatorPlayer(gameplayState.Score))); + this.Push(new SpectatorPlayerLoader(spectatorGameplayState.Score, () => new SoloSpectatorPlayer(spectatorGameplayState.Score))); } } diff --git a/osu.Game/Screens/Spectate/GameplayState.cs b/osu.Game/Screens/Spectate/SpectatorGameplayState.cs similarity index 81% rename from osu.Game/Screens/Spectate/GameplayState.cs rename to osu.Game/Screens/Spectate/SpectatorGameplayState.cs index 4579b9c07c..6ca1ac9a0a 100644 --- a/osu.Game/Screens/Spectate/GameplayState.cs +++ b/osu.Game/Screens/Spectate/SpectatorGameplayState.cs @@ -8,9 +8,9 @@ using osu.Game.Scoring; namespace osu.Game.Screens.Spectate { /// - /// The gameplay state of a spectated user. This class is immutable. + /// An immutable spectator gameplay state. /// - public class GameplayState + public class SpectatorGameplayState { /// /// The score which the user is playing. @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Spectate /// public readonly WorkingBeatmap Beatmap; - public GameplayState(Score score, Ruleset ruleset, WorkingBeatmap beatmap) + public SpectatorGameplayState(Score score, Ruleset ruleset, WorkingBeatmap beatmap) { Score = score; Ruleset = ruleset; diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index f0a68ea078..71bcc336f3 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Spectate private readonly IBindableDictionary playingUserStates = new BindableDictionary(); private readonly Dictionary userMap = new Dictionary(); - private readonly Dictionary gameplayStates = new Dictionary(); + private readonly Dictionary gameplayStates = new Dictionary(); private IBindable> managerUpdated; @@ -173,7 +173,7 @@ namespace osu.Game.Screens.Spectate Replay = new Replay { HasReceivedAllFrames = false }, }; - var gameplayState = new GameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap)); + var gameplayState = new SpectatorGameplayState(score, resolvedRuleset, beatmaps.GetWorkingBeatmap(resolvedBeatmap)); gameplayStates[userId] = gameplayState; Schedule(() => StartGameplay(userId, gameplayState)); @@ -190,8 +190,8 @@ namespace osu.Game.Screens.Spectate /// Starts gameplay for a user. /// /// The user to start gameplay for. - /// The gameplay state. - protected abstract void StartGameplay(int userId, [NotNull] GameplayState gameplayState); + /// The gameplay state. + protected abstract void StartGameplay(int userId, [NotNull] SpectatorGameplayState spectatorGameplayState); /// /// Ends gameplay for a user. From 32afd3f4267df99a69b5f11909d37d5e39a78525 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 2 Oct 2021 02:22:23 +0900 Subject: [PATCH 144/391] Replace all basic usages --- .../Mods/TestSceneOsuModHidden.cs | 4 +- .../TestSceneGameplayCursor.cs | 10 ++-- .../Skinning/Legacy/LegacyCursorParticles.cs | 6 +- .../UI/Cursor/OsuCursorContainer.cs | 6 +- .../Skinning/Legacy/LegacyTaikoScroller.cs | 6 +- .../UI/DrawableTaikoMascot.cs | 6 +- .../Gameplay/TestSceneReplayRecorder.cs | 7 ++- .../Gameplay/TestSceneReplayRecording.cs | 7 ++- .../Gameplay/TestSceneSpectatorPlayback.cs | 4 +- osu.Game/Online/Spectator/SpectatorClient.cs | 4 +- osu.Game/Rulesets/UI/ReplayRecorder.cs | 4 +- osu.Game/Screens/Play/GameplayBeatmap.cs | 56 ------------------- osu.Game/Screens/Play/GameplayState.cs | 39 +++++++++++++ osu.Game/Screens/Play/Player.cs | 29 +++++----- osu.Game/Screens/Play/ReplayPlayer.cs | 4 +- osu.Game/Screens/Play/SpectatorPlayer.cs | 4 +- osu.Game/Tests/Visual/TestPlayer.cs | 2 +- 17 files changed, 94 insertions(+), 104 deletions(-) delete mode 100644 osu.Game/Screens/Play/GameplayBeatmap.cs create mode 100644 osu.Game/Screens/Play/GameplayState.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs index 1ac3ad9194..af64be78f8 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs @@ -4,13 +4,11 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods @@ -122,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4; private bool objectWithIncreasedVisibilityHasIndex(int index) - => Player.Mods.Value.OfType().Single().FirstObject == Player.ChildrenOfType().Single().HitObjects[index]; + => Player.Mods.Value.OfType().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index]; private class TestOsuModHidden : OsuModHidden { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index f9dc9abd75..41d9bf7132 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -17,6 +17,7 @@ using osu.Framework.Testing.Input; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Screens.Play; @@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Tests public class TestSceneGameplayCursor : OsuSkinnableTestScene { [Cached] - private GameplayBeatmap gameplayBeatmap; + private GameplayState gameplayState; private OsuCursorContainer lastContainer; @@ -40,7 +41,8 @@ namespace osu.Game.Rulesets.Osu.Tests public TestSceneGameplayCursor() { - gameplayBeatmap = new GameplayBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)); + var ruleset = new OsuRuleset(); + gameplayState = new GameplayState(CreateBeatmap(ruleset.RulesetInfo), ruleset, Array.Empty()); AddStep("change background colour", () => { @@ -57,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddSliderStep("circle size", 0f, 10f, 0f, val => { config.SetValue(OsuSetting.AutoCursorSize, true); - gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = val; + gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = val; Scheduler.AddOnce(() => loadContent(false)); }); @@ -73,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests public void TestSizing(int circleSize, float userScale) { AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale)); - AddStep($"adjust cs to {circleSize}", () => gameplayBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize); + AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize); AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true)); AddStep("load content", () => loadContent()); diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs index c2db5f3f82..611ddd08eb 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private OsuPlayfield playfield { get; set; } [Resolved(canBeNull: true)] - private GameplayBeatmap gameplayBeatmap { get; set; } + private GameplayState gameplayState { get; set; } [BackgroundDependencyLoader] private void load(ISkinSource skin, OsuColour colours) @@ -75,12 +75,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected override void Update() { - if (playfield == null || gameplayBeatmap == null) return; + if (playfield == null || gameplayState == null) return; DrawableHitObject kiaiHitObject = null; // Check whether currently in a kiai section first. This is only done as an optimisation to avoid enumerating AliveObjects when not necessary. - if (gameplayBeatmap.ControlPointInfo.EffectPointAt(Time.Current).KiaiMode) + if (gameplayState.Beatmap.ControlPointInfo.EffectPointAt(Time.Current).KiaiMode) kiaiHitObject = playfield.HitObjectContainer.AliveObjects.FirstOrDefault(isTracking); kiaiSpewer.Active.Value = kiaiHitObject != null; diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index 83bcc88e5f..cfe83d0106 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } [Resolved(canBeNull: true)] - private GameplayBeatmap beatmap { get; set; } + private GameplayState state { get; set; } [Resolved] private OsuConfigManager config { get; set; } @@ -96,10 +96,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { float scale = userCursorScale.Value; - if (autoCursorScale.Value && beatmap != null) + if (autoCursorScale.Value && state != null) { // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier. - scale *= GetScaleForCircleSize(beatmap.BeatmapInfo.BaseDifficulty.CircleSize); + scale *= GetScaleForCircleSize(state.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize); } cursorScale.Value = scale; diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs index 6fc59ea0e8..fa49242675 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs @@ -25,10 +25,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy } [BackgroundDependencyLoader(true)] - private void load(GameplayBeatmap gameplayBeatmap) + private void load(GameplayState gameplayState) { - if (gameplayBeatmap != null) - ((IBindable)LastResult).BindTo(gameplayBeatmap.LastJudgementResult); + if (gameplayState != null) + ((IBindable)LastResult).BindTo(gameplayState.LastJudgementResult); } private bool passing; diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index 6a16f311bf..e1063e1071 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.UI } [BackgroundDependencyLoader(true)] - private void load(TextureStore textures, GameplayBeatmap gameplayBeatmap) + private void load(TextureStore textures, GameplayState gameplayState) { InternalChildren = new[] { @@ -49,8 +49,8 @@ namespace osu.Game.Rulesets.Taiko.UI animations[TaikoMascotAnimationState.Fail] = new TaikoMascotAnimation(TaikoMascotAnimationState.Fail), }; - if (gameplayBeatmap != null) - ((IBindable)LastResult).BindTo(gameplayBeatmap.LastJudgementResult); + if (gameplayState != null) + ((IBindable)LastResult).BindTo(gameplayState.LastJudgementResult); } protected override void LoadComplete() diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index 0a3fedaf8e..d89fd322d1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -17,6 +18,8 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Replays; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; using osu.Game.Scoring; @@ -38,7 +41,7 @@ namespace osu.Game.Tests.Visual.Gameplay private TestReplayRecorder recorder; [Cached] - private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap()); + private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty()); [SetUp] public void SetUp() => Schedule(() => @@ -57,7 +60,7 @@ namespace osu.Game.Tests.Visual.Gameplay Recorder = recorder = new TestReplayRecorder(new Score { Replay = replay, - ScoreInfo = { Beatmap = gameplayBeatmap.BeatmapInfo } + ScoreInfo = { Beatmap = gameplayState.Beatmap.BeatmapInfo } }) { ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs index dfd5e2dc58..07514ad51a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.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.Framework.Allocation; using osu.Framework.Graphics; @@ -13,6 +14,8 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Replays; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; using osu.Game.Scoring; @@ -30,7 +33,7 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly TestRulesetInputManager recordingManager; [Cached] - private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap()); + private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty()); public TestSceneReplayRecording() { @@ -48,7 +51,7 @@ namespace osu.Game.Tests.Visual.Gameplay Recorder = new TestReplayRecorder(new Score { Replay = replay, - ScoreInfo = { Beatmap = gameplayBeatmap.BeatmapInfo } + ScoreInfo = { Beatmap = gameplayState.Beatmap.BeatmapInfo } }) { ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 6f5f774758..07ff35f77b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -25,6 +25,8 @@ using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Replays.Legacy; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.UI; @@ -62,7 +64,7 @@ namespace osu.Game.Tests.Visual.Gameplay private SpectatorClient spectatorClient { get; set; } [Cached] - private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap()); + private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty()); [SetUp] public void SetUp() => Schedule(() => diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index 8c617784b9..d55ad45ff5 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -134,7 +134,7 @@ namespace osu.Game.Online.Spectator return Task.CompletedTask; } - public void BeginPlaying(GameplayBeatmap beatmap, Score score) + public void BeginPlaying(GameplayState state, Score score) { Debug.Assert(ThreadSafety.IsUpdateThread); @@ -148,7 +148,7 @@ namespace osu.Game.Online.Spectator currentState.RulesetID = score.ScoreInfo.RulesetID; currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); - currentBeatmap = beatmap.PlayableBeatmap; + currentBeatmap = state.Beatmap; currentScore = score; BeginPlayingInternal(currentState); diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index b57c224059..976f95cef8 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.UI private SpectatorClient spectatorClient { get; set; } [Resolved] - private GameplayBeatmap gameplayBeatmap { get; set; } + private GameplayState gameplayState { get; set; } protected ReplayRecorder(Score target) { @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.UI inputManager = GetContainingInputManager(); - spectatorClient?.BeginPlaying(gameplayBeatmap, target); + spectatorClient?.BeginPlaying(gameplayState, target); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Play/GameplayBeatmap.cs b/osu.Game/Screens/Play/GameplayBeatmap.cs deleted file mode 100644 index 74fbe540fa..0000000000 --- a/osu.Game/Screens/Play/GameplayBeatmap.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. - -using System.Collections.Generic; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Beatmaps.Timing; -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects; - -namespace osu.Game.Screens.Play -{ - public class GameplayBeatmap : Component, IBeatmap - { - public readonly IBeatmap PlayableBeatmap; - - public GameplayBeatmap(IBeatmap playableBeatmap) - { - PlayableBeatmap = playableBeatmap; - } - - public BeatmapInfo BeatmapInfo - { - get => PlayableBeatmap.BeatmapInfo; - set => PlayableBeatmap.BeatmapInfo = value; - } - - public BeatmapMetadata Metadata => PlayableBeatmap.Metadata; - - public ControlPointInfo ControlPointInfo - { - get => PlayableBeatmap.ControlPointInfo; - set => PlayableBeatmap.ControlPointInfo = value; - } - - public List Breaks => PlayableBeatmap.Breaks; - - public double TotalBreakTime => PlayableBeatmap.TotalBreakTime; - - public IReadOnlyList HitObjects => PlayableBeatmap.HitObjects; - - public IEnumerable GetStatistics() => PlayableBeatmap.GetStatistics(); - - public double GetMostCommonBeatLength() => PlayableBeatmap.GetMostCommonBeatLength(); - - public IBeatmap Clone() => PlayableBeatmap.Clone(); - - private readonly Bindable lastJudgementResult = new Bindable(); - - public IBindable LastJudgementResult => lastJudgementResult; - - public void ApplyResult(JudgementResult result) => lastJudgementResult.Value = result; - } -} diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs new file mode 100644 index 0000000000..4944d5b8e2 --- /dev/null +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -0,0 +1,39 @@ +// 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; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mods; + +#nullable enable + +namespace osu.Game.Screens.Play +{ + public class GameplayState + { + /// + /// The final post-convert post-mod-application beatmap. + /// + public readonly IBeatmap Beatmap; + + public readonly Ruleset Ruleset; + + public IReadOnlyList Mods; + + public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList mods) + { + Beatmap = beatmap; + Ruleset = ruleset; + Mods = mods; + } + + private readonly Bindable lastJudgementResult = new Bindable(); + + public IBindable LastJudgementResult => lastJudgementResult; + + public void ApplyResult(JudgementResult result) => lastJudgementResult.Value = result; + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9927467bd6..a05a8f5056 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -93,9 +93,9 @@ namespace osu.Game.Screens.Play [Resolved] private SpectatorClient spectatorClient { get; set; } - protected Ruleset GameplayRuleset { get; private set; } + public GameplayState GameplayState { get; private set; } - protected GameplayBeatmap GameplayBeatmap { get; private set; } + private Ruleset ruleset; private Sample sampleRestart; @@ -165,7 +165,7 @@ namespace osu.Game.Screens.Play // ensure the score is in a consistent state with the current player. Score.ScoreInfo.Beatmap = Beatmap.Value.BeatmapInfo; - Score.ScoreInfo.Ruleset = GameplayRuleset.RulesetInfo; + Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; Score.ScoreInfo.Mods = Mods.Value.ToArray(); PrepareReplay(); @@ -206,16 +206,16 @@ namespace osu.Game.Screens.Play if (game is OsuGame osuGame) LocalUserPlaying.BindTo(osuGame.LocalUserPlaying); - DrawableRuleset = GameplayRuleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); + DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); dependencies.CacheAs(DrawableRuleset); - ScoreProcessor = GameplayRuleset.CreateScoreProcessor(); + ScoreProcessor = ruleset.CreateScoreProcessor(); ScoreProcessor.ApplyBeatmap(playableBeatmap); ScoreProcessor.Mods.BindTo(Mods); dependencies.CacheAs(ScoreProcessor); - HealthProcessor = GameplayRuleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); + HealthProcessor = ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); HealthProcessor.ApplyBeatmap(playableBeatmap); dependencies.CacheAs(HealthProcessor); @@ -225,12 +225,11 @@ namespace osu.Game.Screens.Play InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); - AddInternal(GameplayBeatmap = new GameplayBeatmap(playableBeatmap)); + dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, Mods.Value)); + AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); - dependencies.CacheAs(GameplayBeatmap); - - var rulesetSkinProvider = new RulesetSkinProvidingContainer(GameplayRuleset, playableBeatmap, Beatmap.Value.Skin); + var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin); // load the skinning hierarchy first. // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. @@ -280,7 +279,7 @@ namespace osu.Game.Screens.Play { HealthProcessor.ApplyResult(r); ScoreProcessor.ApplyResult(r); - GameplayBeatmap.ApplyResult(r); + GameplayState.ApplyResult(r); }; DrawableRuleset.RevertResult += r => @@ -478,17 +477,17 @@ namespace osu.Game.Screens.Play throw new InvalidOperationException("Beatmap was not loaded"); var rulesetInfo = Ruleset.Value ?? Beatmap.Value.BeatmapInfo.Ruleset; - GameplayRuleset = rulesetInfo.CreateInstance(); + ruleset = rulesetInfo.CreateInstance(); try { - playable = Beatmap.Value.GetPlayableBeatmap(GameplayRuleset.RulesetInfo, Mods.Value); + playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Mods.Value); } catch (BeatmapInvalidForRulesetException) { // A playable beatmap may not be creatable with the user's preferred ruleset, so try using the beatmap's default ruleset rulesetInfo = Beatmap.Value.BeatmapInfo.Ruleset; - GameplayRuleset = rulesetInfo.CreateInstance(); + ruleset = rulesetInfo.CreateInstance(); playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, Mods.Value); } @@ -1010,7 +1009,7 @@ namespace osu.Game.Screens.Play using (var stream = new MemoryStream()) { - new LegacyScoreEncoder(score, GameplayBeatmap.PlayableBeatmap).Encode(stream); + new LegacyScoreEncoder(score, GameplayState.Beatmap).Encode(stream); replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 0c6f1ed911..eefea737cf 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play DrawableRuleset?.SetReplayScore(Score); } - protected override Score CreateScore() => createScore(GameplayBeatmap.PlayableBeatmap, Mods.Value); + protected override Score CreateScore() => createScore(GameplayState.Beatmap, Mods.Value); // Don't re-import replay scores as they're already present in the database. protected override Task ImportScore(Score score) => Task.CompletedTask; @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Play void keyboardSeek(int direction) { - double target = Math.Clamp(GameplayClockContainer.CurrentTime + direction * keyboard_seek_amount, 0, GameplayBeatmap.HitObjects.Last().GetEndTime()); + double target = Math.Clamp(GameplayClockContainer.CurrentTime + direction * keyboard_seek_amount, 0, GameplayState.Beatmap.HitObjects.Last().GetEndTime()); Seek(target); } diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index d7e42a9cd1..fbb4fb5699 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -66,8 +66,8 @@ namespace osu.Game.Screens.Play foreach (var frame in bundle.Frames) { - IConvertibleReplayFrame convertibleFrame = GameplayRuleset.CreateConvertibleReplayFrame(); - convertibleFrame.FromLegacy(frame, GameplayBeatmap.PlayableBeatmap); + IConvertibleReplayFrame convertibleFrame = GameplayState.Ruleset.CreateConvertibleReplayFrame(); + convertibleFrame.FromLegacy(frame, GameplayState.Beatmap); var convertedFrame = (ReplayFrame)convertibleFrame; convertedFrame.Time = frame.Time; diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index 5e5f20b307..d68984b144 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual if (autoplayMod != null) { - DrawableRuleset?.SetReplayScore(autoplayMod.CreateReplayScore(GameplayBeatmap.PlayableBeatmap, Mods.Value)); + DrawableRuleset?.SetReplayScore(autoplayMod.CreateReplayScore(GameplayState.Beatmap, Mods.Value)); return; } From 7e009f616845718cc124c68b900ad4c57382a7fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 2 Oct 2021 02:28:35 +0900 Subject: [PATCH 145/391] Add full xmldoc --- osu.Game/Screens/Play/GameplayState.cs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 4944d5b8e2..ba08c946d2 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -12,6 +12,9 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Screens.Play { + /// + /// The state of an active gameplay session, generally constructed and exposed by . + /// public class GameplayState { /// @@ -19,10 +22,23 @@ namespace osu.Game.Screens.Play /// public readonly IBeatmap Beatmap; + /// + /// The ruleset used in gameplay. + /// public readonly Ruleset Ruleset; + /// + /// The mods applied to the gameplay. + /// public IReadOnlyList Mods; + /// + /// A bindable tracking the last judgement result applied to any hit object. + /// + public IBindable LastJudgementResult => lastJudgementResult; + + private readonly Bindable lastJudgementResult = new Bindable(); + public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList mods) { Beatmap = beatmap; @@ -30,10 +46,10 @@ namespace osu.Game.Screens.Play Mods = mods; } - private readonly Bindable lastJudgementResult = new Bindable(); - - public IBindable LastJudgementResult => lastJudgementResult; - + /// + /// Applies the score change of a to this . + /// + /// The to apply. public void ApplyResult(JudgementResult result) => lastJudgementResult.Value = result; } } From 6d6fda833705efad08906e015c564f7a6b621f3f Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Fri, 1 Oct 2021 19:54:10 +0200 Subject: [PATCH 146/391] Fix some usages of `SettingsTextBox` using a bindable with `null` as default --- osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs | 6 +++++- osu.Game.Tournament/Components/DateTextBox.cs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs index e3dae9c27e..d530e1f796 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsSource.cs @@ -53,7 +53,11 @@ namespace osu.Game.Tests.Visual.Settings }; [SettingSource("Sample string", "Change something for a mod")] - public Bindable StringBindable { get; } = new Bindable(); + public Bindable StringBindable { get; } = new Bindable + { + Default = string.Empty, + Value = "Sample text" + }; } private enum TestEnum diff --git a/osu.Game.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs index 5782301a65..2237e389d7 100644 --- a/osu.Game.Tournament/Components/DateTextBox.cs +++ b/osu.Game.Tournament/Components/DateTextBox.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tournament.Components public DateTextBox() { - base.Current = new Bindable(); + base.Current = new Bindable(string.Empty); ((OsuTextBox)Control).OnCommit += (sender, newText) => { From dcd7d7a709beb3b5b837b4c88c9861c764613072 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 2 Oct 2021 03:05:04 +0900 Subject: [PATCH 147/391] Add `JsonIgnore` rule for `StoryboardFile` Not sure why this is required, doesn't make much sense. --- osu.Game/Beatmaps/BeatmapSetInfo.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 4804c7032c..9f5a07ec43 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using JetBrains.Annotations; +using Newtonsoft.Json; using osu.Framework.Testing; using osu.Game.Database; @@ -61,6 +62,7 @@ namespace osu.Game.Beatmaps public string Hash { get; set; } + [JsonIgnore] public string StoryboardFile => ((IBeatmapSetInfo)this).StoryboardFile; /// From 5ea51f4a9ff1d001882f41409f2adef11df4e396 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 Oct 2021 18:15:07 +0000 Subject: [PATCH 148/391] Bump Sentry from 3.9.0 to 3.9.4 Bumps [Sentry](https://github.com/getsentry/sentry-dotnet) from 3.9.0 to 3.9.4. - [Release notes](https://github.com/getsentry/sentry-dotnet/releases) - [Changelog](https://github.com/getsentry/sentry-dotnet/blob/main/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-dotnet/compare/3.9.0...3.9.4) --- updated-dependencies: - dependency-name: Sentry dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 3e0809c359..ff89fadcc3 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -38,7 +38,7 @@ - + From a5025dc8b89ed0fdea683c2f8e0fa682577063c9 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Fri, 1 Oct 2021 22:23:16 +0100 Subject: [PATCH 149/391] Buff base multiplier --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index d0ef0e67da..f90b5a679d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -110,9 +110,7 @@ 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. if (mods.Any(m => m is OsuModBlinds)) - aimValue *= 1.0 + (0.14 + totalHits * (0.0016 / (1 + 2 * countMiss)) - * Math.Pow(accuracy, 16)) - * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate); + aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * countMiss)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate); else if (mods.Any(h => h is OsuModHidden)) aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); From 9517d69f21fdd51ce7539eed07b42f7ff6a9f4ca Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Oct 2021 00:59:31 +0000 Subject: [PATCH 150/391] Bump MessagePack from 2.3.75 to 2.3.85 Bumps [MessagePack](https://github.com/neuecc/MessagePack-CSharp) from 2.3.75 to 2.3.85. - [Release notes](https://github.com/neuecc/MessagePack-CSharp/releases) - [Changelog](https://github.com/neuecc/MessagePack-CSharp/blob/master/prepare_release.ps1) - [Commits](https://github.com/neuecc/MessagePack-CSharp/compare/v2.3.75...v2.3.85) --- updated-dependencies: - dependency-name: MessagePack dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 ff89fadcc3..c110aadac1 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,7 +22,7 @@ - + From f60f712bcc232177f909d98e4ab686478c6c364b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Oct 2021 00:59:32 +0000 Subject: [PATCH 151/391] Bump Microsoft.AspNetCore.SignalR.Protocols.MessagePack Bumps [Microsoft.AspNetCore.SignalR.Protocols.MessagePack](https://github.com/dotnet/aspnetcore) from 5.0.9 to 5.0.10. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Commits](https://github.com/dotnet/aspnetcore/compare/v5.0.9...v5.0.10) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.SignalR.Protocols.MessagePack dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 ff89fadcc3..868074a32f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,7 +24,7 @@ - + From cb8165ca504f426c9b0b2013d6c58a28d73bfcb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Oct 2021 00:59:32 +0000 Subject: [PATCH 152/391] Bump Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson Bumps [Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson](https://github.com/dotnet/aspnetcore) from 5.0.9 to 5.0.10. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Commits](https://github.com/dotnet/aspnetcore/compare/v5.0.9...v5.0.10) --- updated-dependencies: - dependency-name: Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- 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 ff89fadcc3..73b95b60d5 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -25,7 +25,7 @@ - + From 973c31132be80ccfc14cac0621fc871db987f406 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 2 Oct 2021 12:44:22 +0900 Subject: [PATCH 153/391] Rename `BeatmapInfo` variables which were named `beatmap` for clarity --- .../Background/TestSceneUserDimBackgrounds.cs | 2 +- .../SongSelect/TestSceneAdvancedStats.cs | 24 ++++---- .../SongSelect/TestSceneBeatmapCarousel.cs | 22 +++---- .../SongSelect/TestScenePlaySongSelect.cs | 60 +++++++++---------- osu.Game.Tournament/Components/SongBar.cs | 32 +++++----- .../Screens/BeatmapInfoScreen.cs | 2 +- osu.Game/Overlays/BeatmapSet/BasicStats.cs | 20 +++---- .../BeatmapSet/BeatmapSetHeaderContent.cs | 2 +- osu.Game/Overlays/BeatmapSet/Details.cs | 10 ++-- osu.Game/Screens/Select/BeatmapCarousel.cs | 14 ++--- osu.Game/Screens/Select/BeatmapDetails.cs | 2 +- .../Select/Carousel/CarouselBeatmap.cs | 54 ++++++++--------- .../Select/Carousel/CarouselBeatmapSet.cs | 6 +- .../Carousel/DrawableCarouselBeatmap.cs | 2 +- .../Carousel/FilterableDifficultyIcon.cs | 2 +- .../FilterableGroupedDifficultyIcon.cs | 2 +- .../Select/Carousel/SetPanelContent.cs | 2 +- .../Screens/Select/Details/AdvancedStats.cs | 20 +++---- osu.Game/Screens/Select/SongSelect.cs | 2 +- 19 files changed, 140 insertions(+), 140 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 1670d86545..12a85c3f26 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -286,7 +286,7 @@ namespace osu.Game.Tests.Visual.Background private void setupUserSettings() { AddUntilStep("Song select is current", () => songSelect.IsCurrentScreen()); - AddUntilStep("Song select has selection", () => songSelect.Carousel?.SelectedBeatmap != null); + AddUntilStep("Song select has selection", () => songSelect.Carousel?.SelectedBeatmapInfo != null); AddStep("Set default user settings", () => { SelectedMods.Value = SelectedMods.Value.Concat(new[] { new OsuModNoFail() }).ToArray(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs index dcc2111ad3..4538e36c5e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestNoMod() { - AddStep("set beatmap", () => advancedStats.Beatmap = exampleBeatmapInfo); + AddStep("set beatmap", () => advancedStats.BeatmapInfo = exampleBeatmapInfo); AddStep("no mods selected", () => SelectedMods.Value = Array.Empty()); @@ -65,7 +65,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestManiaFirstBarText() { - AddStep("set beatmap", () => advancedStats.Beatmap = new BeatmapInfo + AddStep("set beatmap", () => advancedStats.BeatmapInfo = new BeatmapInfo { Ruleset = rulesets.GetRuleset(3), BaseDifficulty = new BeatmapDifficulty @@ -84,11 +84,11 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestEasyMod() { - AddStep("set beatmap", () => advancedStats.Beatmap = exampleBeatmapInfo); + AddStep("set beatmap", () => advancedStats.BeatmapInfo = exampleBeatmapInfo); AddStep("select EZ mod", () => { - var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); + var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance(); SelectedMods.Value = new[] { ruleset.CreateMod() }; }); @@ -101,11 +101,11 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestHardRockMod() { - AddStep("set beatmap", () => advancedStats.Beatmap = exampleBeatmapInfo); + AddStep("set beatmap", () => advancedStats.BeatmapInfo = exampleBeatmapInfo); AddStep("select HR mod", () => { - var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); + var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance(); SelectedMods.Value = new[] { ruleset.CreateMod() }; }); @@ -118,13 +118,13 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestUnchangedDifficultyAdjustMod() { - AddStep("set beatmap", () => advancedStats.Beatmap = exampleBeatmapInfo); + AddStep("set beatmap", () => advancedStats.BeatmapInfo = exampleBeatmapInfo); AddStep("select unchanged Difficulty Adjust mod", () => { - var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); + var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance(); var difficultyAdjustMod = ruleset.CreateMod(); - difficultyAdjustMod.ReadFromDifficulty(advancedStats.Beatmap.BaseDifficulty); + difficultyAdjustMod.ReadFromDifficulty(advancedStats.BeatmapInfo.BaseDifficulty); SelectedMods.Value = new[] { difficultyAdjustMod }; }); @@ -137,13 +137,13 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestChangedDifficultyAdjustMod() { - AddStep("set beatmap", () => advancedStats.Beatmap = exampleBeatmapInfo); + AddStep("set beatmap", () => advancedStats.BeatmapInfo = exampleBeatmapInfo); AddStep("select changed Difficulty Adjust mod", () => { - var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); + var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance(); var difficultyAdjustMod = ruleset.CreateMod(); - var originalDifficulty = advancedStats.Beatmap.BaseDifficulty; + var originalDifficulty = advancedStats.BeatmapInfo.BaseDifficulty; difficultyAdjustMod.ReadFromDifficulty(originalDifficulty); difficultyAdjustMod.DrainRate.Value = originalDifficulty.DrainRate - 0.5f; diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 78ddfa9ed2..66f15670f5 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.SongSelect private readonly Stack selectedSets = new Stack(); private readonly HashSet eagerSelectedIDs = new HashSet(); - private BeatmapInfo currentSelection => carousel.SelectedBeatmap; + private BeatmapInfo currentSelection => carousel.SelectedBeatmapInfo; private const int set_count = 5; @@ -75,11 +75,11 @@ namespace osu.Game.Tests.Visual.SongSelect { for (int i = 0; i < 3; i++) { - AddStep("store selection", () => selection = carousel.SelectedBeatmap); + AddStep("store selection", () => selection = carousel.SelectedBeatmapInfo); if (isIterating) - AddUntilStep("selection changed", () => carousel.SelectedBeatmap != selection); + AddUntilStep("selection changed", () => carousel.SelectedBeatmapInfo != selection); else - AddUntilStep("selection not changed", () => carousel.SelectedBeatmap == selection); + AddUntilStep("selection not changed", () => carousel.SelectedBeatmapInfo == selection); } } } @@ -387,7 +387,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Set non-empty mode filter", () => carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(1) }, false)); - AddAssert("Something is selected", () => carousel.SelectedBeatmap != null); + AddAssert("Something is selected", () => carousel.SelectedBeatmapInfo != null); } /// @@ -562,7 +562,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("filter to ruleset 0", () => carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false)); AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false)); - AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmap.RulesetID == 0); + AddAssert("unfiltered beatmap not selected", () => carousel.SelectedBeatmapInfo.RulesetID == 0); AddStep("remove mixed set", () => { @@ -653,7 +653,7 @@ namespace osu.Game.Tests.Visual.SongSelect carousel.Filter(new FilterCriteria { SearchText = Guid.NewGuid().ToString() }, false); }); - AddAssert("selection lost", () => carousel.SelectedBeatmap == null); + AddAssert("selection lost", () => carousel.SelectedBeatmapInfo == null); AddStep("Restore different ruleset filter", () => { @@ -661,7 +661,7 @@ namespace osu.Game.Tests.Visual.SongSelect eagerSelectedIDs.Add(carousel.SelectedBeatmapSet.ID); }); - AddAssert("selection changed", () => carousel.SelectedBeatmap != manySets.First().Beatmaps.First()); + AddAssert("selection changed", () => carousel.SelectedBeatmapInfo != manySets.First().Beatmaps.First()); } AddAssert("Selection was random", () => eagerSelectedIDs.Count > 2); @@ -763,9 +763,9 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep($"selected is set{set}{(diff.HasValue ? $" diff{diff.Value}" : "")}", () => { if (diff != null) - return carousel.SelectedBeatmap == carousel.BeatmapSets.Skip(set - 1).First().Beatmaps.Skip(diff.Value - 1).First(); + return carousel.SelectedBeatmapInfo == carousel.BeatmapSets.Skip(set - 1).First().Beatmaps.Skip(diff.Value - 1).First(); - return carousel.BeatmapSets.Skip(set - 1).First().Beatmaps.Contains(carousel.SelectedBeatmap); + return carousel.BeatmapSets.Skip(set - 1).First().Beatmaps.Contains(carousel.SelectedBeatmapInfo); }); private void setSelected(int set, int diff) => @@ -800,7 +800,7 @@ namespace osu.Game.Tests.Visual.SongSelect { carousel.RandomAlgorithm.Value = RandomSelectAlgorithm.RandomPermutation; - if (!selectedSets.Any() && carousel.SelectedBeatmap != null) + if (!selectedSets.Any() && carousel.SelectedBeatmapInfo != null) selectedSets.Push(carousel.SelectedBeatmapSet); carousel.SelectNextRandom(); diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 102e5ee425..f9e81d3da6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select next and enter", () => { InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType() - .First(b => ((CarouselBeatmap)b.Item).Beatmap != songSelect.Carousel.SelectedBeatmap)); + .First(b => ((CarouselBeatmap)b.Item).BeatmapInfo != songSelect.Carousel.SelectedBeatmapInfo)); InputManager.Click(MouseButton.Left); @@ -172,7 +172,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select next and enter", () => { InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType() - .First(b => ((CarouselBeatmap)b.Item).Beatmap != songSelect.Carousel.SelectedBeatmap)); + .First(b => ((CarouselBeatmap)b.Item).BeatmapInfo != songSelect.Carousel.SelectedBeatmapInfo)); InputManager.PressButton(MouseButton.Left); @@ -312,7 +312,7 @@ namespace osu.Game.Tests.Visual.SongSelect { createSongSelect(); addRulesetImportStep(2); - AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null); + AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); } [Test] @@ -322,13 +322,13 @@ namespace osu.Game.Tests.Visual.SongSelect changeRuleset(2); addRulesetImportStep(2); addRulesetImportStep(1); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.RulesetID == 2); changeRuleset(1); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 1); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.RulesetID == 1); changeRuleset(0); - AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null); + AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); } [Test] @@ -338,7 +338,7 @@ namespace osu.Game.Tests.Visual.SongSelect changeRuleset(2); addRulesetImportStep(2); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.RulesetID == 2); addRulesetImportStep(0); addRulesetImportStep(0); @@ -355,7 +355,7 @@ namespace osu.Game.Tests.Visual.SongSelect Beatmap.Value = manager.GetWorkingBeatmap(target); }); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target)); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.Equals(target)); // this is an important check, to make sure updateComponentFromBeatmap() was actually run AddUntilStep("selection shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap.BeatmapInfo.Equals(target)); @@ -368,7 +368,7 @@ namespace osu.Game.Tests.Visual.SongSelect changeRuleset(2); addRulesetImportStep(2); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.RulesetID == 2); addRulesetImportStep(0); addRulesetImportStep(0); @@ -385,7 +385,7 @@ namespace osu.Game.Tests.Visual.SongSelect Ruleset.Value = rulesets.AvailableRulesets.First(r => r.ID == 0); }); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.Equals(target)); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo.Equals(target)); AddUntilStep("has correct ruleset", () => Ruleset.Value.ID == 0); @@ -444,7 +444,7 @@ namespace osu.Game.Tests.Visual.SongSelect { createSongSelect(); addManyTestMaps(); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); bool startRequested = false; @@ -473,13 +473,13 @@ namespace osu.Game.Tests.Visual.SongSelect // used for filter check below AddStep("allow convert display", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType().First().Text = "nonono"); AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap); - AddUntilStep("has no selection", () => songSelect.Carousel.SelectedBeatmap == null); + AddUntilStep("has no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); BeatmapInfo target = null; @@ -494,7 +494,7 @@ namespace osu.Game.Tests.Visual.SongSelect Beatmap.Value = manager.GetWorkingBeatmap(target); }); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); AddAssert("selected only shows expected ruleset (plus converts)", () => { @@ -502,16 +502,16 @@ namespace osu.Game.Tests.Visual.SongSelect // special case for converts checked here. return selectedPanel.ChildrenOfType().All(i => - i.IsFiltered || i.Item.Beatmap.Ruleset.ID == targetRuleset || i.Item.Beatmap.Ruleset.ID == 0); + i.IsFiltered || i.Item.BeatmapInfo.Ruleset.ID == targetRuleset || i.Item.BeatmapInfo.Ruleset.ID == 0); }); - AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmap?.OnlineBeatmapID == target.OnlineBeatmapID); + AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmapInfo?.OnlineBeatmapID == target.OnlineBeatmapID); AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID); AddStep("reset filter text", () => songSelect.FilterControl.ChildrenOfType().First().Text = string.Empty); AddAssert("game still correct", () => Beatmap.Value?.BeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID); - AddAssert("carousel still correct", () => songSelect.Carousel.SelectedBeatmap.OnlineBeatmapID == target.OnlineBeatmapID); + AddAssert("carousel still correct", () => songSelect.Carousel.SelectedBeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID); } [Test] @@ -522,13 +522,13 @@ namespace osu.Game.Tests.Visual.SongSelect changeRuleset(0); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType().First().Text = "nonono"); AddUntilStep("dummy selected", () => Beatmap.Value is DummyWorkingBeatmap); - AddUntilStep("has no selection", () => songSelect.Carousel.SelectedBeatmap == null); + AddUntilStep("has no selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); BeatmapInfo target = null; @@ -540,15 +540,15 @@ namespace osu.Game.Tests.Visual.SongSelect Beatmap.Value = manager.GetWorkingBeatmap(target); }); - AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap != null); + AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmapInfo != null); - AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmap?.OnlineBeatmapID == target.OnlineBeatmapID); + AddUntilStep("carousel has correct", () => songSelect.Carousel.SelectedBeatmapInfo?.OnlineBeatmapID == target.OnlineBeatmapID); AddUntilStep("game has correct", () => Beatmap.Value.BeatmapInfo.OnlineBeatmapID == target.OnlineBeatmapID); AddStep("set filter text", () => songSelect.FilterControl.ChildrenOfType().First().Text = "nononoo"); AddUntilStep("game lost selection", () => Beatmap.Value is DummyWorkingBeatmap); - AddAssert("carousel lost selection", () => songSelect.Carousel.SelectedBeatmap == null); + AddAssert("carousel lost selection", () => songSelect.Carousel.SelectedBeatmapInfo == null); } [Test] @@ -581,9 +581,9 @@ namespace osu.Game.Tests.Visual.SongSelect createSongSelect(); addRulesetImportStep(0); AddStep("Move to last difficulty", () => songSelect.Carousel.SelectBeatmap(songSelect.Carousel.BeatmapSets.First().Beatmaps.Last())); - AddStep("Store current ID", () => previousID = songSelect.Carousel.SelectedBeatmap.ID); + AddStep("Store current ID", () => previousID = songSelect.Carousel.SelectedBeatmapInfo.ID); AddStep("Hide first beatmap", () => manager.Hide(songSelect.Carousel.SelectedBeatmapSet.Beatmaps.First())); - AddAssert("Selected beatmap has not changed", () => songSelect.Carousel.SelectedBeatmap.ID == previousID); + AddAssert("Selected beatmap has not changed", () => songSelect.Carousel.SelectedBeatmapInfo.ID == previousID); } [Test] @@ -641,7 +641,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Click(MouseButton.Left); }); - AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmap == filteredBeatmap); + AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmapInfo == filteredBeatmap); } [Test] @@ -717,7 +717,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Find an icon for different ruleset", () => { difficultyIcon = set.ChildrenOfType() - .First(icon => icon.Item.Beatmap.Ruleset.ID == 3); + .First(icon => icon.Item.BeatmapInfo.Ruleset.ID == 3); }); AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); @@ -735,7 +735,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3); - AddAssert("Selected beatmap still same set", () => songSelect.Carousel.SelectedBeatmap.BeatmapSet.ID == previousSetID); + AddAssert("Selected beatmap still same set", () => songSelect.Carousel.SelectedBeatmapInfo.BeatmapSet.ID == previousSetID); AddAssert("Selected beatmap is mania", () => Beatmap.Value.BeatmapInfo.Ruleset.ID == 3); } @@ -767,7 +767,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Find group icon for different ruleset", () => { groupIcon = set.ChildrenOfType() - .First(icon => icon.Items.First().Beatmap.Ruleset.ID == 3); + .First(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3); }); AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); @@ -781,7 +781,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3); - AddAssert("Check first item in group selected", () => Beatmap.Value.BeatmapInfo.Equals(groupIcon.Items.First().Beatmap)); + AddAssert("Check first item in group selected", () => Beatmap.Value.BeatmapInfo.Equals(groupIcon.Items.First().BeatmapInfo)); } [Test] @@ -856,7 +856,7 @@ namespace osu.Game.Tests.Visual.SongSelect private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.FindIndex(b => b == info); - private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmap); + private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmapInfo); private int getDifficultyIconIndex(DrawableCarouselBeatmapSet set, FilterableDifficultyIcon icon) { diff --git a/osu.Game.Tournament/Components/SongBar.cs b/osu.Game.Tournament/Components/SongBar.cs index 6080f7b636..357c82df61 100644 --- a/osu.Game.Tournament/Components/SongBar.cs +++ b/osu.Game.Tournament/Components/SongBar.cs @@ -21,22 +21,22 @@ namespace osu.Game.Tournament.Components { public class SongBar : CompositeDrawable { - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; public const float HEIGHT = 145 / 2f; [Resolved] private IBindable ruleset { get; set; } - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (beatmap == value) + if (beatmapInfo == value) return; - beatmap = value; + beatmapInfo = value; update(); } } @@ -95,18 +95,18 @@ namespace osu.Game.Tournament.Components private void update() { - if (beatmap == null) + if (beatmapInfo == null) { flow.Clear(); return; } - var bpm = beatmap.BeatmapSet.OnlineInfo.BPM; - var length = beatmap.Length; + var bpm = beatmapInfo.BeatmapSet.OnlineInfo.BPM; + var length = beatmapInfo.Length; string hardRockExtra = ""; string srExtra = ""; - var ar = beatmap.BaseDifficulty.ApproachRate; + var ar = beatmapInfo.BaseDifficulty.ApproachRate; if ((mods & LegacyMods.HardRock) > 0) { @@ -132,9 +132,9 @@ namespace osu.Game.Tournament.Components default: stats = new (string heading, string content)[] { - ("CS", $"{beatmap.BaseDifficulty.CircleSize:0.#}{hardRockExtra}"), + ("CS", $"{beatmapInfo.BaseDifficulty.CircleSize:0.#}{hardRockExtra}"), ("AR", $"{ar:0.#}{hardRockExtra}"), - ("OD", $"{beatmap.BaseDifficulty.OverallDifficulty:0.#}{hardRockExtra}"), + ("OD", $"{beatmapInfo.BaseDifficulty.OverallDifficulty:0.#}{hardRockExtra}"), }; break; @@ -142,15 +142,15 @@ namespace osu.Game.Tournament.Components case 3: stats = new (string heading, string content)[] { - ("OD", $"{beatmap.BaseDifficulty.OverallDifficulty:0.#}{hardRockExtra}"), - ("HP", $"{beatmap.BaseDifficulty.DrainRate:0.#}{hardRockExtra}") + ("OD", $"{beatmapInfo.BaseDifficulty.OverallDifficulty:0.#}{hardRockExtra}"), + ("HP", $"{beatmapInfo.BaseDifficulty.DrainRate:0.#}{hardRockExtra}") }; break; case 2: stats = new (string heading, string content)[] { - ("CS", $"{beatmap.BaseDifficulty.CircleSize:0.#}{hardRockExtra}"), + ("CS", $"{beatmapInfo.BaseDifficulty.CircleSize:0.#}{hardRockExtra}"), ("AR", $"{ar:0.#}"), }; break; @@ -186,7 +186,7 @@ namespace osu.Game.Tournament.Components Children = new Drawable[] { new DiffPiece(stats), - new DiffPiece(("Star Rating", $"{beatmap.StarDifficulty:0.#}{srExtra}")) + new DiffPiece(("Star Rating", $"{beatmapInfo.StarDifficulty:0.#}{srExtra}")) } }, new FillFlowContainer @@ -229,7 +229,7 @@ namespace osu.Game.Tournament.Components } } }, - new TournamentBeatmapPanel(beatmap) + new TournamentBeatmapPanel(beatmapInfo) { RelativeSizeAxes = Axes.X, Width = 0.5f, diff --git a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs index 50498304ca..b94b164116 100644 --- a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs +++ b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tournament.Screens private void beatmapChanged(ValueChangedEvent beatmap) { SongBar.FadeInFromZero(300, Easing.OutQuint); - SongBar.Beatmap = beatmap.NewValue; + SongBar.BeatmapInfo = beatmap.NewValue; } } } diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs index 5a6cde8229..683f4f0c49 100644 --- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs +++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs @@ -38,16 +38,16 @@ namespace osu.Game.Overlays.BeatmapSet } } - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (value == beatmap) return; + if (value == beatmapInfo) return; - beatmap = value; + beatmapInfo = value; updateDisplay(); } @@ -57,7 +57,7 @@ namespace osu.Game.Overlays.BeatmapSet { bpm.Value = BeatmapSet?.OnlineInfo?.BPM.ToLocalisableString(@"0.##") ?? (LocalisableString)"-"; - if (beatmap == null) + if (beatmapInfo == null) { length.Value = string.Empty; circleCount.Value = string.Empty; @@ -65,11 +65,11 @@ namespace osu.Game.Overlays.BeatmapSet } else { - length.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(TimeSpan.FromMilliseconds(beatmap.Length).ToFormattedDuration()); - length.Value = TimeSpan.FromMilliseconds(beatmap.Length).ToFormattedDuration(); + length.TooltipText = BeatmapsetsStrings.ShowStatsTotalLength(TimeSpan.FromMilliseconds(beatmapInfo.Length).ToFormattedDuration()); + length.Value = TimeSpan.FromMilliseconds(beatmapInfo.Length).ToFormattedDuration(); - circleCount.Value = beatmap.OnlineInfo.CircleCount.ToLocalisableString(@"N0"); - sliderCount.Value = beatmap.OnlineInfo.SliderCount.ToLocalisableString(@"N0"); + circleCount.Value = beatmapInfo.OnlineInfo.CircleCount.ToLocalisableString(@"N0"); + sliderCount.Value = beatmapInfo.OnlineInfo.SliderCount.ToLocalisableString(@"N0"); } } diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs index c3b6444a24..dcf06ac7fb 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -211,7 +211,7 @@ namespace osu.Game.Overlays.BeatmapSet Picker.Beatmap.ValueChanged += b => { - Details.Beatmap = b.NewValue; + Details.BeatmapInfo = b.NewValue; externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineBeatmapSetID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineBeatmapID}"; }; } diff --git a/osu.Game/Overlays/BeatmapSet/Details.cs b/osu.Game/Overlays/BeatmapSet/Details.cs index 680487ffbb..92361ae4f8 100644 --- a/osu.Game/Overlays/BeatmapSet/Details.cs +++ b/osu.Game/Overlays/BeatmapSet/Details.cs @@ -37,16 +37,16 @@ namespace osu.Game.Overlays.BeatmapSet } } - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (value == beatmap) return; + if (value == beatmapInfo) return; - basic.Beatmap = advanced.Beatmap = beatmap = value; + basic.BeatmapInfo = advanced.BeatmapInfo = beatmapInfo = value; } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 5eceae3c6e..f424587e22 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Select /// /// The currently selected beatmap. /// - public BeatmapInfo SelectedBeatmap => selectedBeatmap?.Beatmap; + public BeatmapInfo SelectedBeatmapInfo => selectedBeatmap?.BeatmapInfo; private CarouselBeatmap selectedBeatmap => selectedBeatmapSet?.Beatmaps.FirstOrDefault(s => s.State.Value == CarouselItemState.Selected); @@ -65,7 +65,7 @@ namespace osu.Game.Screens.Select private CarouselBeatmapSet selectedBeatmapSet; /// - /// Raised when the is changed. + /// Raised when the is changed. /// public Action SelectionChanged; @@ -212,7 +212,7 @@ namespace osu.Game.Screens.Select // If the selected beatmap is about to be removed, store its ID so it can be re-selected if required if (existingSet?.State?.Value == CarouselItemState.Selected) - previouslySelectedID = selectedBeatmap?.Beatmap.ID; + previouslySelectedID = selectedBeatmap?.BeatmapInfo.ID; var newSet = createCarouselSet(beatmapSet); @@ -233,7 +233,7 @@ namespace osu.Game.Screens.Select // check if we can/need to maintain our current selection. if (previouslySelectedID != null) - select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.Beatmap.ID == previouslySelectedID) ?? newSet); + select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.BeatmapInfo.ID == previouslySelectedID) ?? newSet); itemsCache.Invalidate(); Schedule(() => BeatmapSetsChanged?.Invoke()); @@ -258,7 +258,7 @@ namespace osu.Game.Screens.Select if (!bypassFilters && set.Filtered.Value) continue; - var item = set.Beatmaps.FirstOrDefault(p => p.Beatmap.Equals(beatmap)); + var item = set.Beatmaps.FirstOrDefault(p => p.BeatmapInfo.Equals(beatmap)); if (item == null) // The beatmap that needs to be selected doesn't exist in this set @@ -472,7 +472,7 @@ namespace osu.Game.Screens.Select private float? scrollTarget; /// - /// Scroll to the current . + /// Scroll to the current . /// /// /// Whether the scroll position should immediately be shifted to the target, delegating animation to visible panels. @@ -720,7 +720,7 @@ namespace osu.Game.Screens.Select if (state.NewValue == CarouselItemState.Selected) { selectedBeatmapSet = set; - SelectionChanged?.Invoke(c.Beatmap); + SelectionChanged?.Invoke(c.BeatmapInfo); itemsCache.Invalidate(); ScrollToSelected(); diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 973f54c038..d59d76300a 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -170,7 +170,7 @@ namespace osu.Game.Screens.Select private void updateStatistics() { - advanced.Beatmap = Beatmap; + advanced.BeatmapInfo = Beatmap; description.Text = Beatmap?.Version; source.Text = Beatmap?.Metadata?.Source; tags.Text = Beatmap?.Metadata?.Tags; diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index f95ddfee41..3f729d9477 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -12,11 +12,11 @@ namespace osu.Game.Screens.Select.Carousel { public override float TotalHeight => DrawableCarouselBeatmap.HEIGHT; - public readonly BeatmapInfo Beatmap; + public readonly BeatmapInfo BeatmapInfo; - public CarouselBeatmap(BeatmapInfo beatmap) + public CarouselBeatmap(BeatmapInfo beatmapInfo) { - Beatmap = beatmap; + BeatmapInfo = beatmapInfo; State.Value = CarouselItemState.Collapsed; } @@ -28,36 +28,36 @@ namespace osu.Game.Screens.Select.Carousel bool match = criteria.Ruleset == null || - Beatmap.RulesetID == criteria.Ruleset.ID || - (Beatmap.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps); + BeatmapInfo.RulesetID == criteria.Ruleset.ID || + (BeatmapInfo.RulesetID == 0 && criteria.Ruleset.ID > 0 && criteria.AllowConvertedBeatmaps); - if (Beatmap.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) + if (BeatmapInfo.BeatmapSet?.Equals(criteria.SelectedBeatmapSet) == true) { // only check ruleset equality or convertability for selected beatmap Filtered.Value = !match; return; } - match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(Beatmap.StarDifficulty); - match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(Beatmap.BaseDifficulty.ApproachRate); - match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(Beatmap.BaseDifficulty.DrainRate); - match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(Beatmap.BaseDifficulty.CircleSize); - match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(Beatmap.BaseDifficulty.OverallDifficulty); - match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(Beatmap.Length); - match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(Beatmap.BPM); + match &= !criteria.StarDifficulty.HasFilter || criteria.StarDifficulty.IsInRange(BeatmapInfo.StarDifficulty); + match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(BeatmapInfo.BaseDifficulty.ApproachRate); + match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(BeatmapInfo.BaseDifficulty.DrainRate); + match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(BeatmapInfo.BaseDifficulty.CircleSize); + match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(BeatmapInfo.BaseDifficulty.OverallDifficulty); + match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(BeatmapInfo.Length); + match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(BeatmapInfo.BPM); - match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(Beatmap.BeatDivisor); - match &= !criteria.OnlineStatus.HasFilter || criteria.OnlineStatus.IsInRange(Beatmap.Status); + match &= !criteria.BeatDivisor.HasFilter || criteria.BeatDivisor.IsInRange(BeatmapInfo.BeatDivisor); + match &= !criteria.OnlineStatus.HasFilter || criteria.OnlineStatus.IsInRange(BeatmapInfo.Status); - match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(Beatmap.Metadata.AuthorString); - match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(Beatmap.Metadata.Artist) || - criteria.Artist.Matches(Beatmap.Metadata.ArtistUnicode); + match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(BeatmapInfo.Metadata.AuthorString); + match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) || + criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode); - match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(Beatmap.StarDifficulty); + match &= !criteria.UserStarDifficulty.HasFilter || criteria.UserStarDifficulty.IsInRange(BeatmapInfo.StarDifficulty); if (match) { - var terms = Beatmap.SearchableTerms; + var terms = BeatmapInfo.SearchableTerms; foreach (var criteriaTerm in criteria.SearchTerms) match &= terms.Any(term => term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase)); @@ -66,16 +66,16 @@ namespace osu.Game.Screens.Select.Carousel // this should be done after text matching so we can prioritise matching numbers in metadata. if (!match && criteria.SearchNumber.HasValue) { - match = (Beatmap.OnlineBeatmapID == criteria.SearchNumber.Value) || - (Beatmap.BeatmapSet?.OnlineBeatmapSetID == criteria.SearchNumber.Value); + match = (BeatmapInfo.OnlineBeatmapID == criteria.SearchNumber.Value) || + (BeatmapInfo.BeatmapSet?.OnlineBeatmapSetID == criteria.SearchNumber.Value); } } if (match) - match &= criteria.Collection?.Beatmaps.Contains(Beatmap) ?? true; + match &= criteria.Collection?.Beatmaps.Contains(BeatmapInfo) ?? true; if (match && criteria.RulesetCriteria != null) - match &= criteria.RulesetCriteria.Matches(Beatmap); + match &= criteria.RulesetCriteria.Matches(BeatmapInfo); Filtered.Value = !match; } @@ -89,13 +89,13 @@ namespace osu.Game.Screens.Select.Carousel { default: case SortMode.Difficulty: - var ruleset = Beatmap.RulesetID.CompareTo(otherBeatmap.Beatmap.RulesetID); + var ruleset = BeatmapInfo.RulesetID.CompareTo(otherBeatmap.BeatmapInfo.RulesetID); if (ruleset != 0) return ruleset; - return Beatmap.StarDifficulty.CompareTo(otherBeatmap.Beatmap.StarDifficulty); + return BeatmapInfo.StarDifficulty.CompareTo(otherBeatmap.BeatmapInfo.StarDifficulty); } } - public override string ToString() => Beatmap.ToString(); + public override string ToString() => BeatmapInfo.ToString(); } } diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 00c2c2cb4a..0d7882bf17 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -47,8 +47,8 @@ namespace osu.Game.Screens.Select.Carousel { if (LastSelected == null || LastSelected.Filtered.Value) { - if (GetRecommendedBeatmap?.Invoke(Children.OfType().Where(b => !b.Filtered.Value).Select(b => b.Beatmap)) is BeatmapInfo recommended) - return Children.OfType().First(b => b.Beatmap == recommended); + if (GetRecommendedBeatmap?.Invoke(Children.OfType().Where(b => !b.Filtered.Value).Select(b => b.BeatmapInfo)) is BeatmapInfo recommended) + return Children.OfType().First(b => b.BeatmapInfo == recommended); } return base.GetNextToSelect(); @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Select.Carousel /// /// All beatmaps which are not filtered and valid for display. /// - protected IEnumerable ValidBeatmaps => Beatmaps.Where(b => !b.Filtered.Value || b.State.Value == CarouselItemState.Selected).Select(b => b.Beatmap); + protected IEnumerable ValidBeatmaps => Beatmaps.Where(b => !b.Filtered.Value || b.State.Value == CarouselItemState.Selected).Select(b => b.BeatmapInfo); private int compareUsingAggregateMax(CarouselBeatmapSet other, Func func) { diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 633ef9297e..2fe7ff4562 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Select.Carousel public DrawableCarouselBeatmap(CarouselBeatmap panel) { - beatmap = panel.Beatmap; + beatmap = panel.BeatmapInfo; Item = panel; } diff --git a/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs b/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs index 51fe7796c7..ce0cec837b 100644 --- a/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs +++ b/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Select.Carousel public readonly CarouselBeatmap Item; public FilterableDifficultyIcon(CarouselBeatmap item) - : base(item.Beatmap, performBackgroundDifficultyLookup: false) + : base(item.BeatmapInfo, performBackgroundDifficultyLookup: false) { filtered.BindTo(item.Filtered); filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100)); diff --git a/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs b/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs index d2f9ed3a6a..acffdd9f64 100644 --- a/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs +++ b/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Select.Carousel public readonly List Items; public FilterableGroupedDifficultyIcon(List items, RulesetInfo ruleset) - : base(items.Select(i => i.Beatmap).ToList(), ruleset, Color4.White) + : base(items.Select(i => i.BeatmapInfo).ToList(), ruleset, Color4.White) { Items = items; diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 23a02547b2..9fb640ba1a 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Select.Carousel var beatmaps = carouselSet.Beatmaps.ToList(); return beatmaps.Count > maximum_difficulty_icons - ? (IEnumerable)beatmaps.GroupBy(b => b.Beatmap.Ruleset).Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Key)) + ? (IEnumerable)beatmaps.GroupBy(b => b.BeatmapInfo.Ruleset).Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Key)) : beatmaps.Select(b => new FilterableDifficultyIcon(b)); } } diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 53e30fd9ca..8c978e25ae 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -38,16 +38,16 @@ namespace osu.Game.Screens.Select.Details protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate; private readonly StatisticRow starDifficulty; - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (value == beatmap) return; + if (value == beatmapInfo) return; - beatmap = value; + beatmapInfo = value; updateStatistics(); } @@ -106,7 +106,7 @@ namespace osu.Game.Screens.Select.Details private void updateStatistics() { - BeatmapDifficulty baseDifficulty = Beatmap?.BaseDifficulty; + BeatmapDifficulty baseDifficulty = BeatmapInfo?.BaseDifficulty; BeatmapDifficulty adjustedDifficulty = null; if (baseDifficulty != null && mods.Value.Any(m => m is IApplicableToDifficulty)) @@ -117,7 +117,7 @@ namespace osu.Game.Screens.Select.Details mod.ApplyToDifficulty(adjustedDifficulty); } - switch (Beatmap?.Ruleset?.ID ?? 0) + switch (BeatmapInfo?.Ruleset?.ID ?? 0) { case 3: // Account for mania differences locally for now @@ -145,13 +145,13 @@ namespace osu.Game.Screens.Select.Details { starDifficultyCancellationSource?.Cancel(); - if (Beatmap == null) + if (BeatmapInfo == null) return; starDifficultyCancellationSource = new CancellationTokenSource(); - var normalStarDifficulty = difficultyCache.GetDifficultyAsync(Beatmap, ruleset.Value, null, starDifficultyCancellationSource.Token); - var moddedStarDifficulty = difficultyCache.GetDifficultyAsync(Beatmap, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token); + var normalStarDifficulty = difficultyCache.GetDifficultyAsync(BeatmapInfo, ruleset.Value, null, starDifficultyCancellationSource.Token); + var moddedStarDifficulty = difficultyCache.GetDifficultyAsync(BeatmapInfo, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token); Task.WhenAll(normalStarDifficulty, moddedStarDifficulty).ContinueWith(_ => Schedule(() => { diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 9801098952..e4ab360765 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -377,7 +377,7 @@ namespace osu.Game.Screens.Select // avoid attempting to continue before a selection has been obtained. // this could happen via a user interaction while the carousel is still in a loading state. - if (Carousel.SelectedBeatmap == null) return; + if (Carousel.SelectedBeatmapInfo == null) return; if (beatmap != null) Carousel.SelectBeatmap(beatmap); From d55836c0b2bd529735efef5fd281e4da4e023baa Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Sat, 2 Oct 2021 15:10:30 +0200 Subject: [PATCH 154/391] Make `ResetButton` no longer part of search filtering The button will now appear if and only if all the bindings in its section are visible (not filtered out by the search) --- .../Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index 0e8e10c086..806390c0ec 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -75,5 +75,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input Content.CornerRadius = 5; } + + public override IEnumerable FilterTerms => Enumerable.Empty(); } } From 6ec2223b5c231fc747b31e3d9f87ba810d2e376b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 2 Oct 2021 23:01:44 +0900 Subject: [PATCH 155/391] Catch potential file access exceptions also in async flow --- osu.Game.Tests/Database/RealmTest.cs | 38 +++++++++++++--------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs index b7658d6408..219690db30 100644 --- a/osu.Game.Tests/Database/RealmTest.cs +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -39,25 +39,9 @@ namespace osu.Game.Tests.Database realmFactory.Dispose(); - try - { - Logger.Log($"Final database size: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); - } - catch - { - // windows runs may error due to file still being open. - } - + Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); realmFactory.Compact(); - - try - { - Logger.Log($"Final database size after compact: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); - } - catch - { - // windows runs may error due to file still being open. - } + Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}"); } }); } @@ -71,16 +55,28 @@ namespace osu.Game.Tests.Database using (var realmFactory = new RealmContextFactory(testStorage, caller)) { Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); - await testAction(realmFactory, testStorage); realmFactory.Dispose(); - Logger.Log($"Final database size: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); realmFactory.Compact(); - Logger.Log($"Final database size after compact: {testStorage.GetStream(realmFactory.Filename)?.Length ?? 0}"); + Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}"); } }); } + + private static long getFileSize(Storage testStorage, RealmContextFactory realmFactory) + { + try + { + return testStorage.GetStream(realmFactory.Filename)?.Length ?? 0; + } + catch + { + // windows runs may error due to file still being open. + return 0; + } + } } } From ec61c3c5eeb2a72e74930902eccb78bc06abf1ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 Oct 2021 00:55:29 +0900 Subject: [PATCH 156/391] Rename all remaining cases --- osu.Desktop/DiscordRichPresence.cs | 4 +- .../Beatmaps/ManiaBeatmapConverter.cs | 4 +- .../ManiaFilterCriteria.cs | 4 +- .../Beatmaps/IO/ImportBeatmapTest.cs | 6 +- .../NonVisual/Filtering/FilterMatchingTest.cs | 2 +- .../Filtering/FilterQueryParserTest.cs | 2 +- .../Online/TestSceneBeatmapSetOverlay.cs | 2 +- .../TestSceneBeatmapSetOverlaySuccessRate.cs | 8 +-- .../SongSelect/TestSceneBeatmapDetails.cs | 14 ++-- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 26 ++++---- .../TestSceneDeleteLocalScore.cs | 13 ++-- .../TestSceneTournamentBeatmapPanel.cs | 2 +- .../TestSceneTournamentModDisplay.cs | 6 +- .../Components/TournamentBeatmapPanel.cs | 20 +++--- osu.Game.Tournament/IPC/FileBasedIPC.cs | 2 +- .../Screens/Editors/RoundEditorScreen.cs | 2 +- .../Screens/Editors/SeedingEditorScreen.cs | 2 +- .../Screens/MapPool/MapPoolScreen.cs | 6 +- osu.Game.Tournament/TournamentGameBase.cs | 4 +- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 24 +++---- osu.Game/Beatmaps/BeatmapManager.cs | 16 ++--- osu.Game/Beatmaps/BeatmapModelManager.cs | 18 ++--- osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs | 66 +++++++++---------- osu.Game/Beatmaps/BeatmapStore.cs | 24 +++---- osu.Game/Beatmaps/DifficultyRecommender.cs | 6 +- osu.Game/Beatmaps/Drawables/DifficultyIcon.cs | 36 +++++----- .../Drawables/DifficultyIconTooltip.cs | 8 +-- .../Online/API/Requests/GetBeatmapRequest.cs | 8 +-- .../Online/API/Requests/GetScoresRequest.cs | 14 ++-- .../API/Requests/Responses/APIBeatmap.cs | 2 +- .../API/Requests/Responses/APIBeatmapSet.cs | 2 +- .../Requests/Responses/APILegacyScoreInfo.cs | 8 +-- .../Responses/APIUserMostPlayedBeatmap.cs | 8 +-- osu.Game/Online/Chat/NowPlayingCommand.cs | 10 +-- osu.Game/Online/Rooms/APIPlaylistBeatmap.cs | 4 +- osu.Game/Online/Rooms/PlaylistItem.cs | 2 +- osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs | 20 +++--- osu.Game/Overlays/BeatmapSet/Info.cs | 6 +- osu.Game/Overlays/BeatmapSet/SuccessRate.cs | 16 ++--- osu.Game/Overlays/BeatmapSetOverlay.cs | 2 +- .../Sections/BeatmapMetadataContainer.cs | 18 ++--- .../Historical/DrawableMostPlayedBeatmap.cs | 24 +++---- .../Sections/Ranks/DrawableProfileScore.cs | 12 ++-- .../Rulesets/Filter/IRulesetFilterCriteria.cs | 6 +- .../Components/Menus/DifficultyMenuItem.cs | 4 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 8 +-- .../Select/BeatmapClearScoresDialog.cs | 6 +- osu.Game/Screens/Select/BeatmapDetailArea.cs | 2 +- osu.Game/Screens/Select/BeatmapDetails.cs | 38 +++++------ .../Carousel/DrawableCarouselBeatmap.cs | 32 ++++----- .../Screens/Select/Carousel/TopLocalRank.cs | 12 ++-- .../Select/Leaderboards/BeatmapLeaderboard.cs | 20 +++--- .../Screens/Select/LocalScoreDeleteDialog.cs | 4 +- .../Screens/Select/PlayBeatmapDetailArea.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 32 ++++----- osu.Game/Skinning/LegacyBeatmapSkin.cs | 8 +-- .../Beatmaps/LegacyBeatmapSkinColourTest.cs | 4 +- osu.Game/Users/UserActivity.cs | 22 +++---- 58 files changed, 342 insertions(+), 341 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index dcb88efeb6..e2b40e9dc6 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -140,10 +140,10 @@ namespace osu.Desktop switch (activity) { case UserActivity.InGame game: - return game.Beatmap.ToString(); + return game.BeatmapInfo.ToString(); case UserActivity.Editing edit: - return edit.Beatmap.ToString(); + return edit.BeatmapInfo.ToString(); case UserActivity.InLobby lobby: return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 26393c8edb..0321a5325b 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -71,9 +71,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps originalTargetColumns = TargetColumns; } - public static int GetColumnCountForNonConvert(BeatmapInfo beatmap) + public static int GetColumnCountForNonConvert(BeatmapInfo beatmapInfo) { - var roundedCircleSize = Math.Round(beatmap.BaseDifficulty.CircleSize); + var roundedCircleSize = Math.Round(beatmapInfo.BaseDifficulty.CircleSize); return (int)Math.Max(1, roundedCircleSize); } diff --git a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs index d9a278ef29..0290230490 100644 --- a/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs +++ b/osu.Game.Rulesets.Mania/ManiaFilterCriteria.cs @@ -13,9 +13,9 @@ namespace osu.Game.Rulesets.Mania { private FilterCriteria.OptionalRange keys; - public bool Matches(BeatmapInfo beatmap) + public bool Matches(BeatmapInfo beatmapInfo) { - return !keys.HasFilter || (beatmap.RulesetID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmap))); + return !keys.HasFilter || (beatmapInfo.RulesetID == new ManiaRuleset().LegacyID && keys.IsInRange(ManiaBeatmapConverter.GetColumnCountForNonConvert(beatmapInfo))); } public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index cba7f34ede..b536fc61b7 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -945,13 +945,13 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); } - private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmap) + private static Task createScoreForBeatmap(OsuGameBase osu, BeatmapInfo beatmapInfo) { return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2, - Beatmap = beatmap, - BeatmapInfoID = beatmap.ID + Beatmap = beatmapInfo, + BeatmapInfoID = beatmapInfo.ID }, new ImportScoreTest.TestArchiveReader()); } diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index 8ff2743b6a..ed86daf8b6 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -239,7 +239,7 @@ namespace osu.Game.Tests.NonVisual.Filtering match = shouldMatch; } - public bool Matches(BeatmapInfo beatmap) => match; + public bool Matches(BeatmapInfo beatmapInfo) => 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 a55bdd2df8..df42c70c87 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -256,7 +256,7 @@ namespace osu.Game.Tests.NonVisual.Filtering { public string CustomValue { get; set; } - public bool Matches(BeatmapInfo beatmap) => true; + public bool Matches(BeatmapInfo beatmapInfo) => true; public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) { diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index f420ad976b..453e26ef96 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -233,7 +233,7 @@ namespace osu.Game.Tests.Visual.Online }); }); - AddAssert("shown beatmaps of current ruleset", () => overlay.Header.HeaderContent.Picker.Difficulties.All(b => b.Beatmap.Ruleset.Equals(overlay.Header.RulesetSelector.Current.Value))); + AddAssert("shown beatmaps of current ruleset", () => overlay.Header.HeaderContent.Picker.Difficulties.All(b => b.BeatmapInfo.Ruleset.Equals(overlay.Header.RulesetSelector.Current.Value))); AddAssert("left-most beatmap selected", () => overlay.Header.HeaderContent.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected); } diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs index fd5c188b94..fe8e33f783 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs @@ -58,10 +58,10 @@ namespace osu.Game.Tests.Visual.Online var firstBeatmap = createBeatmap(); var secondBeatmap = createBeatmap(); - AddStep("set first set", () => successRate.Beatmap = firstBeatmap); + AddStep("set first set", () => successRate.BeatmapInfo = firstBeatmap); AddAssert("ratings set", () => successRate.Graph.Metrics == firstBeatmap.Metrics); - AddStep("set second set", () => successRate.Beatmap = secondBeatmap); + AddStep("set second set", () => successRate.BeatmapInfo = secondBeatmap); AddAssert("ratings set", () => successRate.Graph.Metrics == secondBeatmap.Metrics); static BeatmapInfo createBeatmap() => new BeatmapInfo @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestOnlyFailMetrics() { - AddStep("set beatmap", () => successRate.Beatmap = new BeatmapInfo + AddStep("set beatmap", () => successRate.BeatmapInfo = new BeatmapInfo { Metrics = new BeatmapMetrics { @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestEmptyMetrics() { - AddStep("set beatmap", () => successRate.Beatmap = new BeatmapInfo + AddStep("set beatmap", () => successRate.BeatmapInfo = new BeatmapInfo { Metrics = new BeatmapMetrics() }); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs index b4544fbc85..d5b4fb9a80 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestAllMetrics() { - AddStep("all metrics", () => details.Beatmap = new BeatmapInfo + AddStep("all metrics", () => details.BeatmapInfo = new BeatmapInfo { BeatmapSet = new BeatmapSetInfo { @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestAllMetricsExceptSource() { - AddStep("all except source", () => details.Beatmap = new BeatmapInfo + AddStep("all except source", () => details.BeatmapInfo = new BeatmapInfo { BeatmapSet = new BeatmapSetInfo { @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestOnlyRatings() { - AddStep("ratings", () => details.Beatmap = new BeatmapInfo + AddStep("ratings", () => details.BeatmapInfo = new BeatmapInfo { BeatmapSet = new BeatmapSetInfo { @@ -117,7 +117,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestOnlyFailsAndRetries() { - AddStep("fails retries", () => details.Beatmap = new BeatmapInfo + AddStep("fails retries", () => details.BeatmapInfo = new BeatmapInfo { Version = "Only Retries and Fails", Metadata = new BeatmapMetadata @@ -144,7 +144,7 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestNoMetrics() { - AddStep("no metrics", () => details.Beatmap = new BeatmapInfo + AddStep("no metrics", () => details.BeatmapInfo = new BeatmapInfo { Version = "No Metrics", Metadata = new BeatmapMetadata @@ -166,13 +166,13 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestNullBeatmap() { - AddStep("null beatmap", () => details.Beatmap = null); + AddStep("null beatmap", () => details.BeatmapInfo = null); } [Test] public void TestOnlineMetrics() { - AddStep("online ratings/retries/fails", () => details.Beatmap = new BeatmapInfo + AddStep("online ratings/retries/fails", () => details.BeatmapInfo = new BeatmapInfo { OnlineBeatmapID = 162, }); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 29815ce9ff..95cf6a9903 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.SongSelect beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); - leaderboard.Beatmap = beatmapInfo; + leaderboard.BeatmapInfo = beatmapInfo; }); clearScores(); @@ -186,7 +186,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void checkCount(int expected) => AddUntilStep("Correct count displayed", () => leaderboard.ChildrenOfType().Count() == expected); - private static ScoreInfo[] generateSampleScores(BeatmapInfo beatmap) + private static ScoreInfo[] generateSampleScores(BeatmapInfo beatmapInfo) { return new[] { @@ -197,7 +197,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 6602580, @@ -216,7 +216,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 4608074, @@ -235,7 +235,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 1014222, @@ -254,7 +254,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 1541390, @@ -273,7 +273,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 2243452, @@ -292,7 +292,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 2705430, @@ -311,7 +311,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 7151382, @@ -330,7 +330,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 2051389, @@ -349,7 +349,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 6169483, @@ -368,7 +368,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmap, + Beatmap = beatmapInfo, User = new User { Id = 6702666, @@ -385,7 +385,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void showBeatmapWithStatus(BeatmapSetOnlineStatus status) { - leaderboard.Beatmap = new BeatmapInfo + leaderboard.BeatmapInfo = new BeatmapInfo { OnlineBeatmapID = 1113057, Status = status, diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 2e30ed9827..f58dbef145 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -37,7 +37,8 @@ namespace osu.Game.Tests.Visual.UserInterface private ScoreManager scoreManager; private readonly List importedScores = new List(); - private BeatmapInfo beatmap; + + private BeatmapInfo beatmapInfo; [Cached] private readonly DialogOverlay dialogOverlay; @@ -55,7 +56,7 @@ namespace osu.Game.Tests.Visual.UserInterface Anchor = Anchor.Centre, Size = new Vector2(550f, 450f), Scope = BeatmapLeaderboardScope.Local, - Beatmap = new BeatmapInfo + BeatmapInfo = new BeatmapInfo { ID = 1, Metadata = new BeatmapMetadata @@ -84,15 +85,15 @@ namespace osu.Game.Tests.Visual.UserInterface dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), Resources, dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory, Scheduler)); - beatmap = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Beatmaps[0]; + beatmapInfo = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Beatmaps[0]; for (int i = 0; i < 50; i++) { var score = new ScoreInfo { OnlineScoreID = i, - Beatmap = beatmap, - BeatmapInfoID = beatmap.ID, + Beatmap = beatmapInfo, + BeatmapInfoID = beatmapInfo.ID, Accuracy = RNG.NextDouble(), TotalScore = RNG.Next(1, 1000000), MaxCombo = RNG.Next(1, 1000), @@ -115,7 +116,7 @@ namespace osu.Game.Tests.Visual.UserInterface leaderboard.Scores = null; leaderboard.FinishTransforms(true); // After setting scores, we may be waiting for transforms to expire drawables - leaderboard.Beatmap = beatmap; + leaderboard.BeatmapInfo = beatmapInfo; leaderboard.RefreshScores(); // Required in the case that the beatmap hasn't changed }); diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs index bc32a12ab7..f9c553cb3f 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentBeatmapPanel.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tournament.Tests.Components private void success(APIBeatmap apiBeatmap) { - var beatmap = apiBeatmap.ToBeatmap(rulesets); + var beatmap = apiBeatmap.ToBeatmapInfo(rulesets); Add(new TournamentBeatmapPanel(beatmap) { Anchor = Anchor.Centre, diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs index 47e7ed9b61..27eb55a9fb 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tournament.Tests.Components private FillFlowContainer fillFlow; - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; [BackgroundDependencyLoader] private void load() @@ -44,12 +44,12 @@ namespace osu.Game.Tournament.Tests.Components private void success(APIBeatmap apiBeatmap) { - beatmap = apiBeatmap.ToBeatmap(rulesets); + beatmapInfo = apiBeatmap.ToBeatmapInfo(rulesets); var mods = rulesets.GetRuleset(Ladder.Ruleset.Value.ID ?? 0).CreateInstance().AllMods; foreach (var mod in mods) { - fillFlow.Add(new TournamentBeatmapPanel(beatmap, mod.Acronym) + fillFlow.Add(new TournamentBeatmapPanel(beatmapInfo, mod.Acronym) { Anchor = Anchor.Centre, Origin = Anchor.Centre diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index e6d73c6e83..0e5a66e7fe 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tournament.Components { public class TournamentBeatmapPanel : CompositeDrawable { - public readonly BeatmapInfo Beatmap; + public readonly BeatmapInfo BeatmapInfo; private readonly string mod; private const float horizontal_padding = 10; @@ -32,11 +32,11 @@ namespace osu.Game.Tournament.Components private readonly Bindable currentMatch = new Bindable(); private Box flash; - public TournamentBeatmapPanel(BeatmapInfo beatmap, string mod = null) + public TournamentBeatmapPanel(BeatmapInfo beatmapInfo, string mod = null) { - if (beatmap == null) throw new ArgumentNullException(nameof(beatmap)); + if (beatmapInfo == null) throw new ArgumentNullException(nameof(beatmapInfo)); - Beatmap = beatmap; + BeatmapInfo = beatmapInfo; this.mod = mod; Width = 400; Height = HEIGHT; @@ -61,7 +61,7 @@ namespace osu.Game.Tournament.Components { RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray(0.5f), - BeatmapSet = Beatmap.BeatmapSet, + BeatmapSet = BeatmapInfo.BeatmapSet, }, new FillFlowContainer { @@ -75,8 +75,8 @@ namespace osu.Game.Tournament.Components new TournamentSpriteText { Text = new RomanisableString( - $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}", - $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}"), + $"{BeatmapInfo.Metadata.ArtistUnicode ?? BeatmapInfo.Metadata.Artist} - {BeatmapInfo.Metadata.TitleUnicode ?? BeatmapInfo.Metadata.Title}", + $"{BeatmapInfo.Metadata.Artist} - {BeatmapInfo.Metadata.Title}"), Font = OsuFont.Torus.With(weight: FontWeight.Bold), }, new FillFlowContainer @@ -93,7 +93,7 @@ namespace osu.Game.Tournament.Components }, new TournamentSpriteText { - Text = Beatmap.Metadata.AuthorString, + Text = BeatmapInfo.Metadata.AuthorString, Padding = new MarginPadding { Right = 20 }, Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14) }, @@ -105,7 +105,7 @@ namespace osu.Game.Tournament.Components }, new TournamentSpriteText { - Text = Beatmap.Version, + Text = BeatmapInfo.Version, Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14) }, } @@ -149,7 +149,7 @@ namespace osu.Game.Tournament.Components private void updateState() { - var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == Beatmap.OnlineBeatmapID); + var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == BeatmapInfo.OnlineBeatmapID); bool doFlash = found != choice; choice = found; diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index f538d4a7d9..7010a30eb7 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tournament.IPC else { beatmapLookupRequest = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = beatmapId }); - beatmapLookupRequest.Success += b => Beatmap.Value = b.ToBeatmap(Rulesets); + beatmapLookupRequest.Success += b => Beatmap.Value = b.ToBeatmapInfo(Rulesets); API.Queue(beatmapLookupRequest); } } diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs index 27ad6650d1..6e4fc8fe1a 100644 --- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs @@ -238,7 +238,7 @@ namespace osu.Game.Tournament.Screens.Editors req.Success += res => { - Model.BeatmapInfo = res.ToBeatmap(rulesets); + Model.BeatmapInfo = res.ToBeatmapInfo(rulesets); updatePanel(); }; diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index 6418bf97da..b64a3993e6 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -246,7 +246,7 @@ namespace osu.Game.Tournament.Screens.Editors req.Success += res => { - Model.BeatmapInfo = res.ToBeatmap(rulesets); + Model.BeatmapInfo = res.ToBeatmapInfo(rulesets); updatePanel(); }; diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index d4292c5492..1e3c550323 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -147,11 +147,11 @@ namespace osu.Game.Tournament.Screens.MapPool if (map != null) { - if (e.Button == MouseButton.Left && map.Beatmap.OnlineBeatmapID != null) - addForBeatmap(map.Beatmap.OnlineBeatmapID.Value); + if (e.Button == MouseButton.Left && map.BeatmapInfo.OnlineBeatmapID != null) + addForBeatmap(map.BeatmapInfo.OnlineBeatmapID.Value); else { - var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.Beatmap.OnlineBeatmapID); + var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.BeatmapInfo.OnlineBeatmapID); if (existing != null) { diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 531da00faf..2e4ed9d5b1 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -182,7 +182,7 @@ namespace osu.Game.Tournament { var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID }); API.Perform(req); - b.BeatmapInfo = req.Result?.ToBeatmap(RulesetStore); + b.BeatmapInfo = req.Result?.ToBeatmapInfo(RulesetStore); addedInfo = true; } @@ -203,7 +203,7 @@ namespace osu.Game.Tournament { var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID }); req.Perform(API); - b.BeatmapInfo = req.Result?.ToBeatmap(RulesetStore); + b.BeatmapInfo = req.Result?.ToBeatmapInfo(RulesetStore); addedInfo = true; } diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 0aa6a6dd0b..c46ab93ece 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -242,7 +242,7 @@ namespace osu.Game.Beatmaps { // GetDifficultyAsync will fall back to existing data from BeatmapInfo if not locally available // (contrary to GetAsync) - GetDifficultyAsync(bindable.Beatmap, rulesetInfo, mods, cancellationToken) + GetDifficultyAsync(bindable.BeatmapInfo, rulesetInfo, mods, cancellationToken) .ContinueWith(t => { // We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events. @@ -262,7 +262,7 @@ namespace osu.Game.Beatmaps private StarDifficulty computeDifficulty(in DifficultyCacheLookup key) { // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. - var beatmapInfo = key.Beatmap; + var beatmapInfo = key.BeatmapInfo; var rulesetInfo = key.Ruleset; try @@ -270,7 +270,7 @@ namespace osu.Game.Beatmaps var ruleset = rulesetInfo.CreateInstance(); Debug.Assert(ruleset != null); - var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.Beatmap)); + var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); var attributes = calculator.Calculate(key.OrderedMods); return new StarDifficulty(attributes); @@ -300,21 +300,21 @@ namespace osu.Game.Beatmaps public readonly struct DifficultyCacheLookup : IEquatable { - public readonly BeatmapInfo Beatmap; + public readonly BeatmapInfo BeatmapInfo; public readonly RulesetInfo Ruleset; public readonly Mod[] OrderedMods; - public DifficultyCacheLookup([NotNull] BeatmapInfo beatmap, [CanBeNull] RulesetInfo ruleset, IEnumerable mods) + public DifficultyCacheLookup([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo ruleset, IEnumerable mods) { - Beatmap = beatmap; + BeatmapInfo = beatmapInfo; // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. - Ruleset = ruleset ?? Beatmap.Ruleset; + Ruleset = ruleset ?? BeatmapInfo.Ruleset; OrderedMods = mods?.OrderBy(m => m.Acronym).Select(mod => mod.DeepClone()).ToArray() ?? Array.Empty(); } public bool Equals(DifficultyCacheLookup other) - => Beatmap.ID == other.Beatmap.ID + => BeatmapInfo.ID == other.BeatmapInfo.ID && Ruleset.ID == other.Ruleset.ID && OrderedMods.SequenceEqual(other.OrderedMods); @@ -322,7 +322,7 @@ namespace osu.Game.Beatmaps { var hashCode = new HashCode(); - hashCode.Add(Beatmap.ID); + hashCode.Add(BeatmapInfo.ID); hashCode.Add(Ruleset.ID); foreach (var mod in OrderedMods) @@ -334,12 +334,12 @@ namespace osu.Game.Beatmaps private class BindableStarDifficulty : Bindable { - public readonly BeatmapInfo Beatmap; + public readonly BeatmapInfo BeatmapInfo; public readonly CancellationToken CancellationToken; - public BindableStarDifficulty(BeatmapInfo beatmap, CancellationToken cancellationToken) + public BindableStarDifficulty(BeatmapInfo beatmapInfo, CancellationToken cancellationToken) { - Beatmap = beatmap; + BeatmapInfo = beatmapInfo; CancellationToken = cancellationToken; } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 2f80633279..a3081cc462 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps private readonly BeatmapModelDownloader beatmapModelDownloader; private readonly WorkingBeatmapCache workingBeatmapCache; - private readonly BeatmapOnlineLookupQueue onlineBetamapLookupQueue; + private readonly BeatmapOnlineLookupQueue onlineBeatmapLookupQueue; public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore resources, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) @@ -48,8 +48,8 @@ namespace osu.Game.Beatmaps if (performOnlineLookups) { - onlineBetamapLookupQueue = new BeatmapOnlineLookupQueue(api, storage); - beatmapModelManager.OnlineLookupQueue = onlineBetamapLookupQueue; + onlineBeatmapLookupQueue = new BeatmapOnlineLookupQueue(api, storage); + beatmapModelManager.OnlineLookupQueue = onlineBeatmapLookupQueue; } } @@ -182,14 +182,14 @@ namespace osu.Game.Beatmaps /// /// Delete a beatmap difficulty. /// - /// The beatmap difficulty to hide. - public void Hide(BeatmapInfo beatmap) => beatmapModelManager.Hide(beatmap); + /// The beatmap difficulty to hide. + public void Hide(BeatmapInfo beatmapInfo) => beatmapModelManager.Hide(beatmapInfo); /// /// Restore a beatmap difficulty. /// - /// The beatmap difficulty to restore. - public void Restore(BeatmapInfo beatmap) => beatmapModelManager.Restore(beatmap); + /// The beatmap difficulty to restore. + public void Restore(BeatmapInfo beatmapInfo) => beatmapModelManager.Restore(beatmapInfo); #endregion @@ -329,7 +329,7 @@ namespace osu.Game.Beatmaps public void Dispose() { - onlineBetamapLookupQueue?.Dispose(); + onlineBeatmapLookupQueue?.Dispose(); } #endregion diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 0beddc1e9b..aa14f95863 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -173,24 +173,24 @@ namespace osu.Game.Beatmaps /// /// Delete a beatmap difficulty. /// - /// The beatmap difficulty to hide. - public void Hide(BeatmapInfo beatmap) => beatmaps.Hide(beatmap); + /// The beatmap difficulty to hide. + public void Hide(BeatmapInfo beatmapInfo) => beatmaps.Hide(beatmapInfo); /// /// Restore a beatmap difficulty. /// - /// The beatmap difficulty to restore. - public void Restore(BeatmapInfo beatmap) => beatmaps.Restore(beatmap); + /// The beatmap difficulty to restore. + public void Restore(BeatmapInfo beatmapInfo) => beatmaps.Restore(beatmapInfo); /// /// Saves an file against a given . /// - /// The to save the content against. The file referenced by will be replaced. + /// The to save the content against. The file referenced by will be replaced. /// The content to write. /// The beatmap content to write, null if to be omitted. - public virtual void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) + public virtual void Save(BeatmapInfo baetmapInfo, IBeatmap beatmapContent, ISkin beatmapSkin = null) { - var setInfo = info.BeatmapSet; + var setInfo = baetmapInfo.BeatmapSet; using (var stream = new MemoryStream()) { @@ -201,7 +201,7 @@ namespace osu.Game.Beatmaps using (ContextFactory.GetForWrite()) { - var beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == info.ID); + var beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == baetmapInfo.ID); var metadata = beatmapInfo.Metadata ?? setInfo.Metadata; // grab the original file (or create a new one if not found). @@ -219,7 +219,7 @@ namespace osu.Game.Beatmaps } } - WorkingBeatmapCache?.Invalidate(info); + WorkingBeatmapCache?.Invalidate(baetmapInfo); } /// diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index 55164e2442..e1faf6005b 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -58,18 +58,18 @@ namespace osu.Game.Beatmaps } // todo: expose this when we need to do individual difficulty lookups. - protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmap, CancellationToken cancellationToken) - => Task.Factory.StartNew(() => lookup(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmapInfo, CancellationToken cancellationToken) + => Task.Factory.StartNew(() => lookup(beatmapSet, beatmapInfo), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); - private void lookup(BeatmapSetInfo set, BeatmapInfo beatmap) + private void lookup(BeatmapSetInfo set, BeatmapInfo beatmapInfo) { - if (checkLocalCache(set, beatmap)) + if (checkLocalCache(set, beatmapInfo)) return; if (api?.State.Value != APIState.Online) return; - var req = new GetBeatmapRequest(beatmap); + var req = new GetBeatmapRequest(beatmapInfo); req.Failure += fail; @@ -82,18 +82,18 @@ namespace osu.Game.Beatmaps if (res != null) { - beatmap.Status = res.Status; - beatmap.BeatmapSet.Status = res.BeatmapSet.Status; - beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; - beatmap.OnlineBeatmapID = res.OnlineBeatmapID; + beatmapInfo.Status = res.Status; + beatmapInfo.BeatmapSet.Status = res.BeatmapSet.Status; + beatmapInfo.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; + beatmapInfo.OnlineBeatmapID = res.OnlineBeatmapID; - if (beatmap.Metadata != null) - beatmap.Metadata.AuthorID = res.AuthorID; + if (beatmapInfo.Metadata != null) + beatmapInfo.Metadata.AuthorID = res.AuthorID; - if (beatmap.BeatmapSet.Metadata != null) - beatmap.BeatmapSet.Metadata.AuthorID = res.AuthorID; + if (beatmapInfo.BeatmapSet.Metadata != null) + beatmapInfo.BeatmapSet.Metadata.AuthorID = res.AuthorID; - logForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); + logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); } } catch (Exception e) @@ -103,8 +103,8 @@ namespace osu.Game.Beatmaps void fail(Exception e) { - beatmap.OnlineBeatmapID = null; - logForModel(set, $"Online retrieval failed for {beatmap} ({e.Message})"); + beatmapInfo.OnlineBeatmapID = null; + logForModel(set, $"Online retrieval failed for {beatmapInfo} ({e.Message})"); } } @@ -149,7 +149,7 @@ namespace osu.Game.Beatmaps cacheDownloadRequest.PerformAsync(); } - private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmap) + private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmapInfo) { // download is in progress (or was, and failed). if (cacheDownloadRequest != null) @@ -159,9 +159,9 @@ namespace osu.Game.Beatmaps if (!storage.Exists(cache_database_name)) return false; - if (string.IsNullOrEmpty(beatmap.MD5Hash) - && string.IsNullOrEmpty(beatmap.Path) - && beatmap.OnlineBeatmapID == null) + if (string.IsNullOrEmpty(beatmapInfo.MD5Hash) + && string.IsNullOrEmpty(beatmapInfo.Path) + && beatmapInfo.OnlineBeatmapID == null) return false; try @@ -174,9 +174,9 @@ namespace osu.Game.Beatmaps { cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved, user_id FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path"; - cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmap.MD5Hash)); - cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmap.OnlineBeatmapID ?? (object)DBNull.Value)); - cmd.Parameters.Add(new SqliteParameter("@Path", beatmap.Path)); + cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmapInfo.MD5Hash)); + cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmapInfo.OnlineBeatmapID ?? (object)DBNull.Value)); + cmd.Parameters.Add(new SqliteParameter("@Path", beatmapInfo.Path)); using (var reader = cmd.ExecuteReader()) { @@ -184,18 +184,18 @@ namespace osu.Game.Beatmaps { var status = (BeatmapSetOnlineStatus)reader.GetByte(2); - beatmap.Status = status; - beatmap.BeatmapSet.Status = status; - beatmap.BeatmapSet.OnlineBeatmapSetID = reader.GetInt32(0); - beatmap.OnlineBeatmapID = reader.GetInt32(1); + beatmapInfo.Status = status; + beatmapInfo.BeatmapSet.Status = status; + beatmapInfo.BeatmapSet.OnlineBeatmapSetID = reader.GetInt32(0); + beatmapInfo.OnlineBeatmapID = reader.GetInt32(1); - if (beatmap.Metadata != null) - beatmap.Metadata.AuthorID = reader.GetInt32(3); + if (beatmapInfo.Metadata != null) + beatmapInfo.Metadata.AuthorID = reader.GetInt32(3); - if (beatmap.BeatmapSet.Metadata != null) - beatmap.BeatmapSet.Metadata.AuthorID = reader.GetInt32(3); + if (beatmapInfo.BeatmapSet.Metadata != null) + beatmapInfo.BeatmapSet.Metadata.AuthorID = reader.GetInt32(3); - logForModel(set, $"Cached local retrieval for {beatmap}."); + logForModel(set, $"Cached local retrieval for {beatmapInfo}."); return true; } } @@ -204,7 +204,7 @@ namespace osu.Game.Beatmaps } catch (Exception ex) { - logForModel(set, $"Cached local retrieval for {beatmap} failed with {ex}."); + logForModel(set, $"Cached local retrieval for {beatmapInfo} failed with {ex}."); } return false; diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index e3214b7c03..197581db88 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -25,40 +25,40 @@ namespace osu.Game.Beatmaps /// /// Hide a in the database. /// - /// The beatmap to hide. + /// The beatmap to hide. /// Whether the beatmap's was changed. - public bool Hide(BeatmapInfo beatmap) + public bool Hide(BeatmapInfo beatmapInfo) { using (ContextFactory.GetForWrite()) { - Refresh(ref beatmap, Beatmaps); + Refresh(ref beatmapInfo, Beatmaps); - if (beatmap.Hidden) return false; + if (beatmapInfo.Hidden) return false; - beatmap.Hidden = true; + beatmapInfo.Hidden = true; } - BeatmapHidden?.Invoke(beatmap); + BeatmapHidden?.Invoke(beatmapInfo); return true; } /// /// Restore a previously hidden . /// - /// The beatmap to restore. + /// The beatmap to restore. /// Whether the beatmap's was changed. - public bool Restore(BeatmapInfo beatmap) + public bool Restore(BeatmapInfo beatmapInfo) { using (ContextFactory.GetForWrite()) { - Refresh(ref beatmap, Beatmaps); + Refresh(ref beatmapInfo, Beatmaps); - if (!beatmap.Hidden) return false; + if (!beatmapInfo.Hidden) return false; - beatmap.Hidden = false; + beatmapInfo.Hidden = false; } - BeatmapRestored?.Invoke(beatmap); + BeatmapRestored?.Invoke(beatmapInfo); return true; } diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index ca910e70b8..b1b1e58ab7 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -62,14 +62,14 @@ namespace osu.Game.Beatmaps if (!recommendedDifficultyMapping.TryGetValue(r, out var recommendation)) continue; - BeatmapInfo beatmap = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b => + BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b => { var difference = b.StarDifficulty - recommendation; return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder }).FirstOrDefault(); - if (beatmap != null) - return beatmap; + if (beatmapInfo != null) + return beatmapInfo; } return null; diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 0751a777d8..880d70aec2 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -37,7 +37,7 @@ namespace osu.Game.Beatmaps.Drawables } [NotNull] - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; [CanBeNull] private readonly RulesetInfo ruleset; @@ -56,26 +56,26 @@ namespace osu.Game.Beatmaps.Drawables /// /// Creates a new with a given and combination. /// - /// The beatmap to show the difficulty of. + /// The beatmap to show the difficulty of. /// The ruleset to show the difficulty with. /// The mods to show the difficulty with. /// Whether to display a tooltip when hovered. - public DifficultyIcon([NotNull] BeatmapInfo beatmap, [CanBeNull] RulesetInfo ruleset, [CanBeNull] IReadOnlyList mods, bool shouldShowTooltip = true) - : this(beatmap, shouldShowTooltip) + public DifficultyIcon([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo ruleset, [CanBeNull] IReadOnlyList mods, bool shouldShowTooltip = true) + : this(beatmapInfo, shouldShowTooltip) { - this.ruleset = ruleset ?? beatmap.Ruleset; + this.ruleset = ruleset ?? beatmapInfo.Ruleset; this.mods = mods ?? Array.Empty(); } /// /// Creates a new that follows the currently-selected ruleset and mods. /// - /// The beatmap to show the difficulty of. + /// The beatmap to show the difficulty of. /// Whether to display a tooltip when hovered. /// Whether to perform difficulty lookup (including calculation if necessary). - public DifficultyIcon([NotNull] BeatmapInfo beatmap, bool shouldShowTooltip = true, bool performBackgroundDifficultyLookup = true) + public DifficultyIcon([NotNull] BeatmapInfo beatmapInfo, bool shouldShowTooltip = true, bool performBackgroundDifficultyLookup = true) { - this.beatmap = beatmap ?? throw new ArgumentNullException(nameof(beatmap)); + this.beatmapInfo = beatmapInfo ?? throw new ArgumentNullException(nameof(beatmapInfo)); this.shouldShowTooltip = shouldShowTooltip; this.performBackgroundDifficultyLookup = performBackgroundDifficultyLookup; @@ -105,7 +105,7 @@ namespace osu.Game.Beatmaps.Drawables Child = background = new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.ForStarDifficulty(beatmap.StarDifficulty) // Default value that will be re-populated once difficulty calculation completes + Colour = colours.ForStarDifficulty(beatmapInfo.StarDifficulty) // Default value that will be re-populated once difficulty calculation completes }, }, new ConstrainedIconContainer @@ -114,27 +114,27 @@ namespace osu.Game.Beatmaps.Drawables Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, // the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment) - Icon = (ruleset ?? beatmap.Ruleset)?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } + Icon = (ruleset ?? beatmapInfo.Ruleset)?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } }, }; if (performBackgroundDifficultyLookup) - iconContainer.Add(new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmap, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0)); + iconContainer.Add(new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmapInfo, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0)); else - difficultyBindable.Value = new StarDifficulty(beatmap.StarDifficulty, 0); + difficultyBindable.Value = new StarDifficulty(beatmapInfo.StarDifficulty, 0); difficultyBindable.BindValueChanged(difficulty => background.Colour = colours.ForStarDifficulty(difficulty.NewValue.Stars)); } ITooltip IHasCustomTooltip.GetCustomTooltip() => new DifficultyIconTooltip(); - DifficultyIconTooltipContent IHasCustomTooltip.TooltipContent => shouldShowTooltip ? new DifficultyIconTooltipContent(beatmap, difficultyBindable) : null; + DifficultyIconTooltipContent IHasCustomTooltip.TooltipContent => shouldShowTooltip ? new DifficultyIconTooltipContent(beatmapInfo, difficultyBindable) : null; private class DifficultyRetriever : Component { public readonly Bindable StarDifficulty = new Bindable(); - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; private readonly RulesetInfo ruleset; private readonly IReadOnlyList mods; @@ -143,9 +143,9 @@ namespace osu.Game.Beatmaps.Drawables [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } - public DifficultyRetriever(BeatmapInfo beatmap, RulesetInfo ruleset, IReadOnlyList mods) + public DifficultyRetriever(BeatmapInfo beatmapInfo, RulesetInfo ruleset, IReadOnlyList mods) { - this.beatmap = beatmap; + this.beatmapInfo = beatmapInfo; this.ruleset = ruleset; this.mods = mods; } @@ -157,8 +157,8 @@ namespace osu.Game.Beatmaps.Drawables { difficultyCancellation = new CancellationTokenSource(); localStarDifficulty = ruleset != null - ? difficultyCache.GetBindableDifficulty(beatmap, ruleset, mods, difficultyCancellation.Token) - : difficultyCache.GetBindableDifficulty(beatmap, difficultyCancellation.Token); + ? difficultyCache.GetBindableDifficulty(beatmapInfo, ruleset, mods, difficultyCancellation.Token) + : difficultyCache.GetBindableDifficulty(beatmapInfo, difficultyCancellation.Token); localStarDifficulty.BindValueChanged(d => { if (d.NewValue is StarDifficulty diff) diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs index 0329e935bc..d4c9f83a0a 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIconTooltip.cs @@ -89,7 +89,7 @@ namespace osu.Game.Beatmaps.Drawables public void SetContent(DifficultyIconTooltipContent content) { - difficultyName.Text = content.Beatmap.Version; + difficultyName.Text = content.BeatmapInfo.Version; starDifficulty.UnbindAll(); starDifficulty.BindTo(content.Difficulty); @@ -109,12 +109,12 @@ namespace osu.Game.Beatmaps.Drawables internal class DifficultyIconTooltipContent { - public readonly BeatmapInfo Beatmap; + public readonly BeatmapInfo BeatmapInfo; public readonly IBindable Difficulty; - public DifficultyIconTooltipContent(BeatmapInfo beatmap, IBindable difficulty) + public DifficultyIconTooltipContent(BeatmapInfo beatmapInfo, IBindable difficulty) { - Beatmap = beatmap; + BeatmapInfo = beatmapInfo; Difficulty = difficulty; } } diff --git a/osu.Game/Online/API/Requests/GetBeatmapRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs index 87925b94c6..901f7365b8 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapRequest.cs @@ -8,13 +8,13 @@ namespace osu.Game.Online.API.Requests { public class GetBeatmapRequest : APIRequest { - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; - public GetBeatmapRequest(BeatmapInfo beatmap) + public GetBeatmapRequest(BeatmapInfo beatmapInfo) { - this.beatmap = beatmap; + this.beatmapInfo = beatmapInfo; } - protected override string Target => $@"beatmaps/lookup?id={beatmap.OnlineBeatmapID}&checksum={beatmap.MD5Hash}&filename={System.Uri.EscapeUriString(beatmap.Path ?? string.Empty)}"; + protected override string Target => $@"beatmaps/lookup?id={beatmapInfo.OnlineBeatmapID}&checksum={beatmapInfo.MD5Hash}&filename={System.Uri.EscapeUriString(beatmapInfo.Path ?? string.Empty)}"; } } diff --git a/osu.Game/Online/API/Requests/GetScoresRequest.cs b/osu.Game/Online/API/Requests/GetScoresRequest.cs index b4e0e44b2c..f3bf690ed5 100644 --- a/osu.Game/Online/API/Requests/GetScoresRequest.cs +++ b/osu.Game/Online/API/Requests/GetScoresRequest.cs @@ -15,20 +15,20 @@ namespace osu.Game.Online.API.Requests { public class GetScoresRequest : APIRequest { - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; private readonly BeatmapLeaderboardScope scope; private readonly RulesetInfo ruleset; private readonly IEnumerable mods; - public GetScoresRequest(BeatmapInfo beatmap, RulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable mods = null) + public GetScoresRequest(BeatmapInfo beatmapInfo, RulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable mods = null) { - if (!beatmap.OnlineBeatmapID.HasValue) + if (!beatmapInfo.OnlineBeatmapID.HasValue) throw new InvalidOperationException($"Cannot lookup a beatmap's scores without having a populated {nameof(BeatmapInfo.OnlineBeatmapID)}."); if (scope == BeatmapLeaderboardScope.Local) throw new InvalidOperationException("Should not attempt to request online scores for a local scoped leaderboard"); - this.beatmap = beatmap; + this.beatmapInfo = beatmapInfo; this.scope = scope; this.ruleset = ruleset ?? throw new ArgumentNullException(nameof(ruleset)); this.mods = mods ?? Array.Empty(); @@ -42,7 +42,7 @@ namespace osu.Game.Online.API.Requests foreach (APILegacyScoreInfo score in r.Scores) { - score.Beatmap = beatmap; + score.BeatmapInfo = beatmapInfo; score.OnlineRulesetID = ruleset.ID.Value; } @@ -50,12 +50,12 @@ namespace osu.Game.Online.API.Requests if (userScore != null) { - userScore.Score.Beatmap = beatmap; + userScore.Score.BeatmapInfo = beatmapInfo; userScore.Score.OnlineRulesetID = ruleset.ID.Value; } } - protected override string Target => $@"beatmaps/{beatmap.OnlineBeatmapID}/scores{createQueryParameters()}"; + protected override string Target => $@"beatmaps/{beatmapInfo.OnlineBeatmapID}/scores{createQueryParameters()}"; private string createQueryParameters() { diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index 7343870dbc..c2a68c8ca1 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -64,7 +64,7 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"max_combo")] private int? maxCombo { get; set; } - public virtual BeatmapInfo ToBeatmap(RulesetStore rulesets) + public virtual BeatmapInfo ToBeatmapInfo(RulesetStore rulesets) { var set = BeatmapSet?.ToBeatmapSet(rulesets); diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index f653a654ca..35963792d0 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -116,7 +116,7 @@ namespace osu.Game.Online.API.Requests.Responses beatmapSet.Beatmaps = beatmaps?.Select(b => { - var beatmap = b.ToBeatmap(rulesets); + var beatmap = b.ToBeatmapInfo(rulesets); beatmap.BeatmapSet = beatmapSet; beatmap.Metadata = beatmapSet.Metadata; return beatmap; diff --git a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs index 567df524b1..18a0db3928 100644 --- a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs @@ -37,7 +37,7 @@ namespace osu.Game.Online.API.Requests.Responses OnlineScoreID = OnlineScoreID, Date = Date, PP = PP, - Beatmap = Beatmap, + Beatmap = BeatmapInfo, RulesetID = OnlineRulesetID, Hash = Replay ? "online" : string.Empty, // todo: temporary? Rank = Rank, @@ -100,7 +100,7 @@ namespace osu.Game.Online.API.Requests.Responses public DateTimeOffset Date { get; set; } [JsonProperty(@"beatmap")] - public BeatmapInfo Beatmap { get; set; } + public BeatmapInfo BeatmapInfo { get; set; } [JsonProperty("accuracy")] public double Accuracy { get; set; } @@ -114,10 +114,10 @@ namespace osu.Game.Online.API.Requests.Responses set { // extract the set ID to its correct place. - Beatmap.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = value.ID }; + BeatmapInfo.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = value.ID }; value.ID = 0; - Beatmap.Metadata = value; + BeatmapInfo.Metadata = value; } } diff --git a/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs index 4614fe29b7..15f67eda47 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs @@ -16,7 +16,7 @@ namespace osu.Game.Online.API.Requests.Responses public int PlayCount { get; set; } [JsonProperty] - private BeatmapInfo beatmap { get; set; } + private BeatmapInfo beatmapInfo { get; set; } [JsonProperty] private APIBeatmapSet beatmapSet { get; set; } @@ -24,9 +24,9 @@ namespace osu.Game.Online.API.Requests.Responses public BeatmapInfo GetBeatmapInfo(RulesetStore rulesets) { BeatmapSetInfo setInfo = beatmapSet.ToBeatmapSet(rulesets); - beatmap.BeatmapSet = setInfo; - beatmap.Metadata = setInfo.Metadata; - return beatmap; + beatmapInfo.BeatmapSet = setInfo; + beatmapInfo.Metadata = setInfo.Metadata; + return beatmapInfo; } } } diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs index 97a2fbdd5c..89eb00a45a 100644 --- a/osu.Game/Online/Chat/NowPlayingCommand.cs +++ b/osu.Game/Online/Chat/NowPlayingCommand.cs @@ -37,27 +37,27 @@ namespace osu.Game.Online.Chat base.LoadComplete(); string verb; - BeatmapInfo beatmap; + BeatmapInfo beatmapInfo; switch (api.Activity.Value) { case UserActivity.InGame game: verb = "playing"; - beatmap = game.Beatmap; + beatmapInfo = game.BeatmapInfo; break; case UserActivity.Editing edit: verb = "editing"; - beatmap = edit.Beatmap; + beatmapInfo = edit.BeatmapInfo; break; default: verb = "listening to"; - beatmap = currentBeatmap.Value.BeatmapInfo; + beatmapInfo = currentBeatmap.Value.BeatmapInfo; break; } - var beatmapString = beatmap.OnlineBeatmapID.HasValue ? $"[{api.WebsiteRootUrl}/b/{beatmap.OnlineBeatmapID} {beatmap}]" : beatmap.ToString(); + var beatmapString = beatmapInfo.OnlineBeatmapID.HasValue ? $"[{api.WebsiteRootUrl}/b/{beatmapInfo.OnlineBeatmapID} {beatmapInfo}]" : beatmapInfo.ToString(); channelManager.PostMessage($"is {verb} {beatmapString}", true, target); Expire(); diff --git a/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs b/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs index 973dccd528..00623282d3 100644 --- a/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs +++ b/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs @@ -13,9 +13,9 @@ namespace osu.Game.Online.Rooms [JsonProperty("checksum")] public string Checksum { get; set; } - public override BeatmapInfo ToBeatmap(RulesetStore rulesets) + public override BeatmapInfo ToBeatmapInfo(RulesetStore rulesets) { - var b = base.ToBeatmap(rulesets); + var b = base.ToBeatmapInfo(rulesets); b.MD5Hash = Checksum; return b; } diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 1d409d4b56..48f1347fa1 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -70,7 +70,7 @@ namespace osu.Game.Online.Rooms public void MapObjects(BeatmapManager beatmaps, RulesetStore rulesets) { - Beatmap.Value ??= apiBeatmap.ToBeatmap(rulesets); + Beatmap.Value ??= apiBeatmap.ToBeatmapInfo(rulesets); Ruleset.Value ??= rulesets.GetRuleset(RulesetID); Ruleset rulesetInstance = Ruleset.Value.CreateInstance(); diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs index 60e341d2ac..3df275c6d3 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapPicker.cs @@ -178,21 +178,21 @@ namespace osu.Game.Overlays.BeatmapSet } starRatingContainer.FadeOut(100); - Beatmap.Value = Difficulties.FirstOrDefault()?.Beatmap; + Beatmap.Value = Difficulties.FirstOrDefault()?.BeatmapInfo; plays.Value = BeatmapSet?.OnlineInfo.PlayCount ?? 0; favourites.Value = BeatmapSet?.OnlineInfo.FavouriteCount ?? 0; updateDifficultyButtons(); } - private void showBeatmap(BeatmapInfo beatmap) + private void showBeatmap(BeatmapInfo beatmapInfo) { - version.Text = beatmap?.Version; + version.Text = beatmapInfo?.Version; } private void updateDifficultyButtons() { - Difficulties.Children.ToList().ForEach(diff => diff.State = diff.Beatmap == Beatmap.Value ? DifficultySelectorState.Selected : DifficultySelectorState.NotSelected); + Difficulties.Children.ToList().ForEach(diff => diff.State = diff.BeatmapInfo == Beatmap.Value ? DifficultySelectorState.Selected : DifficultySelectorState.NotSelected); } public class DifficultiesContainer : FillFlowContainer @@ -216,7 +216,7 @@ namespace osu.Game.Overlays.BeatmapSet private readonly Box backgroundBox; private readonly DifficultyIcon icon; - public readonly BeatmapInfo Beatmap; + public readonly BeatmapInfo BeatmapInfo; public Action OnHovered; public Action OnClicked; @@ -241,9 +241,9 @@ namespace osu.Game.Overlays.BeatmapSet } } - public DifficultySelectorButton(BeatmapInfo beatmap) + public DifficultySelectorButton(BeatmapInfo beatmapInfo) { - Beatmap = beatmap; + BeatmapInfo = beatmapInfo; Size = new Vector2(size); Margin = new MarginPadding { Horizontal = tile_spacing / 2 }; @@ -260,7 +260,7 @@ namespace osu.Game.Overlays.BeatmapSet Alpha = 0.5f } }, - icon = new DifficultyIcon(beatmap, shouldShowTooltip: false) + icon = new DifficultyIcon(beatmapInfo, shouldShowTooltip: false) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -273,7 +273,7 @@ namespace osu.Game.Overlays.BeatmapSet protected override bool OnHover(HoverEvent e) { fadeIn(); - OnHovered?.Invoke(Beatmap); + OnHovered?.Invoke(BeatmapInfo); return base.OnHover(e); } @@ -286,7 +286,7 @@ namespace osu.Game.Overlays.BeatmapSet protected override bool OnClick(ClickEvent e) { - OnClicked?.Invoke(Beatmap); + OnClicked?.Invoke(BeatmapInfo); return base.OnClick(e); } diff --git a/osu.Game/Overlays/BeatmapSet/Info.cs b/osu.Game/Overlays/BeatmapSet/Info.cs index f9b8de9dba..61c660cbaa 100644 --- a/osu.Game/Overlays/BeatmapSet/Info.cs +++ b/osu.Game/Overlays/BeatmapSet/Info.cs @@ -24,10 +24,10 @@ namespace osu.Game.Overlays.BeatmapSet public readonly Bindable BeatmapSet = new Bindable(); - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => successRate.Beatmap; - set => successRate.Beatmap = value; + get => successRate.BeatmapInfo; + set => successRate.BeatmapInfo = value; } public Info() diff --git a/osu.Game/Overlays/BeatmapSet/SuccessRate.cs b/osu.Game/Overlays/BeatmapSet/SuccessRate.cs index cde4589c98..4a9b8244a5 100644 --- a/osu.Game/Overlays/BeatmapSet/SuccessRate.cs +++ b/osu.Game/Overlays/BeatmapSet/SuccessRate.cs @@ -23,16 +23,16 @@ namespace osu.Game.Overlays.BeatmapSet private readonly Bar successRate; private readonly Container percentContainer; - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (value == beatmap) return; + if (value == beatmapInfo) return; - beatmap = value; + beatmapInfo = value; updateDisplay(); } @@ -40,15 +40,15 @@ namespace osu.Game.Overlays.BeatmapSet private void updateDisplay() { - int passCount = beatmap?.OnlineInfo?.PassCount ?? 0; - int playCount = beatmap?.OnlineInfo?.PlayCount ?? 0; + int passCount = beatmapInfo?.OnlineInfo?.PassCount ?? 0; + int playCount = beatmapInfo?.OnlineInfo?.PlayCount ?? 0; var rate = playCount != 0 ? (float)passCount / playCount : 0; successPercent.Text = rate.ToLocalisableString(@"0.#%"); successRate.Length = rate; percentContainer.ResizeWidthTo(successRate.Length, 250, Easing.InOutCubic); - Graph.Metrics = beatmap?.Metrics; + Graph.Metrics = beatmapInfo?.Metrics; } public SuccessRate() diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index bdb3715e73..f987b57d6e 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -61,7 +61,7 @@ namespace osu.Game.Overlays Header.HeaderContent.Picker.Beatmap.ValueChanged += b => { - info.Beatmap = b.NewValue; + info.BeatmapInfo = b.NewValue; ScrollFlow.ScrollToStart(); }; } diff --git a/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs b/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs index a8a4cfc365..7812a81f30 100644 --- a/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/BeatmapMetadataContainer.cs @@ -15,12 +15,12 @@ namespace osu.Game.Overlays.Profile.Sections /// public abstract class BeatmapMetadataContainer : OsuHoverContainer { - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; - protected BeatmapMetadataContainer(BeatmapInfo beatmap) + protected BeatmapMetadataContainer(BeatmapInfo beatmapInfo) : base(HoverSampleSet.Submit) { - this.beatmap = beatmap; + this.beatmapInfo = beatmapInfo; AutoSizeAxes = Axes.Both; } @@ -30,19 +30,19 @@ namespace osu.Game.Overlays.Profile.Sections { Action = () => { - if (beatmap.OnlineBeatmapID != null) - beatmapSetOverlay?.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value); - else if (beatmap.BeatmapSet?.OnlineBeatmapSetID != null) - beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmap.BeatmapSet.OnlineBeatmapSetID.Value); + if (beatmapInfo.OnlineBeatmapID != null) + beatmapSetOverlay?.FetchAndShowBeatmap(beatmapInfo.OnlineBeatmapID.Value); + else if (beatmapInfo.BeatmapSet?.OnlineBeatmapSetID != null) + beatmapSetOverlay?.FetchAndShowBeatmapSet(beatmapInfo.BeatmapSet.OnlineBeatmapSetID.Value); }; Child = new FillFlowContainer { AutoSizeAxes = Axes.Both, - Children = CreateText(beatmap), + Children = CreateText(beatmapInfo), }; } - protected abstract Drawable[] CreateText(BeatmapInfo beatmap); + protected abstract Drawable[] CreateText(BeatmapInfo beatmapInfo); } } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs index a419bef233..2c6fa76ca4 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs @@ -22,12 +22,12 @@ namespace osu.Game.Overlays.Profile.Sections.Historical private const int cover_width = 100; private const int corner_radius = 6; - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; private readonly int playCount; - public DrawableMostPlayedBeatmap(BeatmapInfo beatmap, int playCount) + public DrawableMostPlayedBeatmap(BeatmapInfo beatmapInfo, int playCount) { - this.beatmap = beatmap; + this.beatmapInfo = beatmapInfo; this.playCount = playCount; RelativeSizeAxes = Axes.X; @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { RelativeSizeAxes = Axes.Y, Width = cover_width, - BeatmapSet = beatmap.BeatmapSet, + BeatmapSet = beatmapInfo.BeatmapSet, }, new Container { @@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical Direction = FillDirection.Vertical, Children = new Drawable[] { - new MostPlayedBeatmapMetadataContainer(beatmap), + new MostPlayedBeatmapMetadataContainer(beatmapInfo), new LinkFlowContainer(t => { t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular); @@ -89,7 +89,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical }.With(d => { d.AddText("mapped by "); - d.AddUserLink(beatmap.Metadata.Author); + d.AddUserLink(beatmapInfo.Metadata.Author); }), } }, @@ -120,23 +120,23 @@ namespace osu.Game.Overlays.Profile.Sections.Historical private class MostPlayedBeatmapMetadataContainer : BeatmapMetadataContainer { - public MostPlayedBeatmapMetadataContainer(BeatmapInfo beatmap) - : base(beatmap) + public MostPlayedBeatmapMetadataContainer(BeatmapInfo beatmapInfo) + : base(beatmapInfo) { } - protected override Drawable[] CreateText(BeatmapInfo beatmap) => new Drawable[] + protected override Drawable[] CreateText(BeatmapInfo beatmapInfo) => new Drawable[] { new OsuSpriteText { Text = new RomanisableString( - $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} [{beatmap.Version}] ", - $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} [{beatmap.Version}] "), + $"{beatmapInfo.Metadata.TitleUnicode ?? beatmapInfo.Metadata.Title} [{beatmapInfo.Version}] ", + $"{beatmapInfo.Metadata.Title ?? beatmapInfo.Metadata.TitleUnicode} [{beatmapInfo.Version}] "), Font = OsuFont.GetFont(weight: FontWeight.Bold) }, new OsuSpriteText { - Text = "by " + new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist), + Text = "by " + new RomanisableString(beatmapInfo.Metadata.ArtistUnicode, beatmapInfo.Metadata.Artist), Font = OsuFont.GetFont(weight: FontWeight.Regular) }, }; diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs index 713303285a..c221f070df 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs @@ -245,27 +245,27 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks private class ScoreBeatmapMetadataContainer : BeatmapMetadataContainer { - public ScoreBeatmapMetadataContainer(BeatmapInfo beatmap) - : base(beatmap) + public ScoreBeatmapMetadataContainer(BeatmapInfo beatmapInfo) + : base(beatmapInfo) { } - protected override Drawable[] CreateText(BeatmapInfo beatmap) => new Drawable[] + protected override Drawable[] CreateText(BeatmapInfo beatmapInfo) => new Drawable[] { new OsuSpriteText { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Text = new RomanisableString( - $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} ", - $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} "), + $"{beatmapInfo.Metadata.TitleUnicode ?? beatmapInfo.Metadata.Title} ", + $"{beatmapInfo.Metadata.Title ?? beatmapInfo.Metadata.TitleUnicode} "), Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold, italics: true) }, new OsuSpriteText { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Text = "by " + new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist), + Text = "by " + new RomanisableString(beatmapInfo.Metadata.ArtistUnicode, beatmapInfo.Metadata.Artist), Font = OsuFont.GetFont(size: 12, italics: true) }, }; diff --git a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs index 13cc41f8e0..dd2ad2cbfa 100644 --- a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs +++ b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs @@ -14,15 +14,15 @@ namespace osu.Game.Rulesets.Filter public interface IRulesetFilterCriteria { /// - /// Checks whether the supplied satisfies ruleset-specific custom criteria, + /// Checks whether the supplied satisfies ruleset-specific custom criteria, /// in addition to the ones mandated by song select. /// - /// The beatmap to test the criteria against. + /// The beatmap to test the criteria against. /// /// true if the beatmap matches the ruleset-specific custom filtering criteria, /// false otherwise. /// - bool Matches(BeatmapInfo beatmap); + bool Matches(BeatmapInfo beatmapInfo); /// /// Attempts to parse a single custom keyword criterion, given by the user via the song select search box. diff --git a/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs b/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs index 5f9b72447b..c458b65607 100644 --- a/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs +++ b/osu.Game/Screens/Edit/Components/Menus/DifficultyMenuItem.cs @@ -10,12 +10,12 @@ namespace osu.Game.Screens.Edit.Components.Menus { public class DifficultyMenuItem : StatefulMenuItem { - public BeatmapInfo Beatmap { get; } + public BeatmapInfo BeatmapInfo { get; } public DifficultyMenuItem(BeatmapInfo beatmapInfo, bool selected, Action difficultyChangeFunc) : base(beatmapInfo.Version ?? "(unnamed)", null) { - Beatmap = beatmapInfo; + BeatmapInfo = beatmapInfo; State.Value = selected; if (!selected) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index f424587e22..e5e28d2fde 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -242,15 +242,15 @@ namespace osu.Game.Screens.Select /// /// Selects a given beatmap on the carousel. /// - /// The beatmap to select. + /// The beatmap to select. /// Whether to select the beatmap even if it is filtered (i.e., not visible on carousel). /// True if a selection was made, False if it wasn't. - public bool SelectBeatmap(BeatmapInfo beatmap, bool bypassFilters = true) + public bool SelectBeatmap(BeatmapInfo beatmapInfo, bool bypassFilters = true) { // ensure that any pending events from BeatmapManager have been run before attempting a selection. Scheduler.Update(); - if (beatmap?.Hidden != false) + if (beatmapInfo?.Hidden != false) return false; foreach (CarouselBeatmapSet set in beatmapSets) @@ -258,7 +258,7 @@ namespace osu.Game.Screens.Select if (!bypassFilters && set.Filtered.Value) continue; - var item = set.Beatmaps.FirstOrDefault(p => p.BeatmapInfo.Equals(beatmap)); + var item = set.Beatmaps.FirstOrDefault(p => p.BeatmapInfo.Equals(beatmapInfo)); if (item == null) // The beatmap that needs to be selected doesn't exist in this set diff --git a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs index b32416b361..8c33b1ea0b 100644 --- a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs +++ b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs @@ -17,9 +17,9 @@ namespace osu.Game.Screens.Select [Resolved] private ScoreManager scoreManager { get; set; } - public BeatmapClearScoresDialog(BeatmapInfo beatmap, Action onCompletion) + public BeatmapClearScoresDialog(BeatmapInfo beatmapInfo, Action onCompletion) { - BodyText = $@"{beatmap.Metadata?.Artist} - {beatmap.Metadata?.Title}"; + BodyText = $@"{beatmapInfo.Metadata?.Artist} - {beatmapInfo.Metadata?.Title}"; Icon = FontAwesome.Solid.Eraser; HeaderText = @"Clearing all local scores. Are you sure?"; Buttons = new PopupDialogButton[] @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Select Text = @"Yes. Please.", Action = () => { - Task.Run(() => scoreManager.Delete(scoreManager.QueryScores(s => !s.DeletePending && s.Beatmap.ID == beatmap.ID).ToList())) + Task.Run(() => scoreManager.Delete(scoreManager.QueryScores(s => !s.DeletePending && s.Beatmap.ID == beatmapInfo.ID).ToList())) .ContinueWith(_ => onCompletion); } }, diff --git a/osu.Game/Screens/Select/BeatmapDetailArea.cs b/osu.Game/Screens/Select/BeatmapDetailArea.cs index 89ae92ec91..72c2ba708b 100644 --- a/osu.Game/Screens/Select/BeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/BeatmapDetailArea.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Select { beatmap = value; - Details.Beatmap = value?.BeatmapInfo; + Details.BeatmapInfo = value?.BeatmapInfo; } } diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index d59d76300a..6ace92370c 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -41,16 +41,16 @@ namespace osu.Game.Screens.Select [Resolved] private RulesetStore rulesets { get; set; } - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (value == beatmap) return; + if (value == beatmapInfo) return; - beatmap = value; + beatmapInfo = value; Scheduler.AddOnce(updateStatistics); } @@ -170,26 +170,26 @@ namespace osu.Game.Screens.Select private void updateStatistics() { - advanced.BeatmapInfo = Beatmap; - description.Text = Beatmap?.Version; - source.Text = Beatmap?.Metadata?.Source; - tags.Text = Beatmap?.Metadata?.Tags; + advanced.BeatmapInfo = BeatmapInfo; + description.Text = BeatmapInfo?.Version; + source.Text = BeatmapInfo?.Metadata?.Source; + tags.Text = BeatmapInfo?.Metadata?.Tags; // metrics may have been previously fetched - if (Beatmap?.BeatmapSet?.Metrics != null && Beatmap?.Metrics != null) + if (BeatmapInfo?.BeatmapSet?.Metrics != null && BeatmapInfo?.Metrics != null) { updateMetrics(); return; } // for now, let's early abort if an OnlineBeatmapID is not present (should have been populated at import time). - if (Beatmap?.OnlineBeatmapID == null || api.State.Value == APIState.Offline) + if (BeatmapInfo?.OnlineBeatmapID == null || api.State.Value == APIState.Offline) { updateMetrics(); return; } - var requestedBeatmap = Beatmap; + var requestedBeatmap = BeatmapInfo; var lookup = new GetBeatmapRequest(requestedBeatmap); @@ -197,11 +197,11 @@ namespace osu.Game.Screens.Select { Schedule(() => { - if (beatmap != requestedBeatmap) + if (beatmapInfo != requestedBeatmap) // the beatmap has been changed since we started the lookup. return; - var b = res.ToBeatmap(rulesets); + var b = res.ToBeatmapInfo(rulesets); if (requestedBeatmap.BeatmapSet == null) requestedBeatmap.BeatmapSet = b.BeatmapSet; @@ -218,7 +218,7 @@ namespace osu.Game.Screens.Select { Schedule(() => { - if (beatmap != requestedBeatmap) + if (beatmapInfo != requestedBeatmap) // the beatmap has been changed since we started the lookup. return; @@ -232,12 +232,12 @@ namespace osu.Game.Screens.Select private void updateMetrics() { - var hasRatings = beatmap?.BeatmapSet?.Metrics?.Ratings?.Any() ?? false; - var hasRetriesFails = (beatmap?.Metrics?.Retries?.Any() ?? false) || (beatmap?.Metrics?.Fails?.Any() ?? false); + var hasRatings = beatmapInfo?.BeatmapSet?.Metrics?.Ratings?.Any() ?? false; + var hasRetriesFails = (beatmapInfo?.Metrics?.Retries?.Any() ?? false) || (beatmapInfo?.Metrics?.Fails?.Any() ?? false); if (hasRatings) { - ratings.Metrics = beatmap.BeatmapSet.Metrics; + ratings.Metrics = beatmapInfo.BeatmapSet.Metrics; ratings.FadeIn(transition_duration); } else @@ -249,7 +249,7 @@ namespace osu.Game.Screens.Select if (hasRetriesFails) { - failRetryGraph.Metrics = beatmap.Metrics; + failRetryGraph.Metrics = beatmapInfo.Metrics; failRetryContainer.FadeIn(transition_duration); } else diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 2fe7ff4562..5940911d4a 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Select.Carousel private const float height = MAX_HEIGHT * 0.6f; - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; private Sprite background; @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Select.Carousel public DrawableCarouselBeatmap(CarouselBeatmap panel) { - beatmap = panel.BeatmapInfo; + beatmapInfo = panel.BeatmapInfo; Item = panel; } @@ -109,7 +109,7 @@ namespace osu.Game.Screens.Select.Carousel Origin = Anchor.CentreLeft, Children = new Drawable[] { - new DifficultyIcon(beatmap, shouldShowTooltip: false) + new DifficultyIcon(beatmapInfo, shouldShowTooltip: false) { Scale = new Vector2(1.8f), }, @@ -129,7 +129,7 @@ namespace osu.Game.Screens.Select.Carousel { new OsuSpriteText { - Text = beatmap.Version, + Text = beatmapInfo.Version, Font = OsuFont.GetFont(size: 20), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft @@ -142,7 +142,7 @@ namespace osu.Game.Screens.Select.Carousel }, new OsuSpriteText { - Text = $"{(beatmap.Metadata ?? beatmap.BeatmapSet.Metadata).Author.Username}", + Text = $"{(beatmapInfo.Metadata ?? beatmapInfo.BeatmapSet.Metadata).Author.Username}", Font = OsuFont.GetFont(italics: true), Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft @@ -156,7 +156,7 @@ namespace osu.Game.Screens.Select.Carousel AutoSizeAxes = Axes.Both, Children = new Drawable[] { - new TopLocalRank(beatmap) + new TopLocalRank(beatmapInfo) { Scale = new Vector2(0.8f), Size = new Vector2(40, 20) @@ -200,7 +200,7 @@ namespace osu.Game.Screens.Select.Carousel protected override bool OnClick(ClickEvent e) { if (Item.State.Value == CarouselItemState.Selected) - startRequested?.Invoke(beatmap); + startRequested?.Invoke(beatmapInfo); return base.OnClick(e); } @@ -216,7 +216,7 @@ namespace osu.Game.Screens.Select.Carousel if (Item.State.Value != CarouselItemState.Collapsed) { // We've potentially cancelled the computation above so a new bindable is required. - starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); + starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmapInfo, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); starDifficultyBindable.BindValueChanged(d => { starCounter.Current = (float)(d.NewValue?.Stars ?? 0); @@ -233,13 +233,13 @@ namespace osu.Game.Screens.Select.Carousel List items = new List(); if (startRequested != null) - items.Add(new OsuMenuItem("Play", MenuItemType.Highlighted, () => startRequested(beatmap))); + items.Add(new OsuMenuItem("Play", MenuItemType.Highlighted, () => startRequested(beatmapInfo))); if (editRequested != null) - items.Add(new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested(beatmap))); + items.Add(new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested(beatmapInfo))); - if (beatmap.OnlineBeatmapID.HasValue && beatmapOverlay != null) - items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value))); + if (beatmapInfo.OnlineBeatmapID.HasValue && beatmapOverlay != null) + items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmapInfo.OnlineBeatmapID.Value))); if (collectionManager != null) { @@ -251,7 +251,7 @@ namespace osu.Game.Screens.Select.Carousel } if (hideRequested != null) - items.Add(new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested(beatmap))); + items.Add(new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested(beatmapInfo))); return items.ToArray(); } @@ -262,12 +262,12 @@ namespace osu.Game.Screens.Select.Carousel return new ToggleMenuItem(collection.Name.Value, MenuItemType.Standard, s => { if (s) - collection.Beatmaps.Add(beatmap); + collection.Beatmaps.Add(beatmapInfo); else - collection.Beatmaps.Remove(beatmap); + collection.Beatmaps.Remove(beatmapInfo); }) { - State = { Value = collection.Beatmaps.Contains(beatmap) } + State = { Value = collection.Beatmaps.Contains(beatmapInfo) } }; } diff --git a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs index 3ad57c1cb0..f2485587d8 100644 --- a/osu.Game/Screens/Select/Carousel/TopLocalRank.cs +++ b/osu.Game/Screens/Select/Carousel/TopLocalRank.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Select.Carousel { public class TopLocalRank : UpdateableRank { - private readonly BeatmapInfo beatmap; + private readonly BeatmapInfo beatmapInfo; [Resolved] private ScoreManager scores { get; set; } @@ -31,10 +31,10 @@ namespace osu.Game.Screens.Select.Carousel private IBindable> itemUpdated; private IBindable> itemRemoved; - public TopLocalRank(BeatmapInfo beatmap) + public TopLocalRank(BeatmapInfo beatmapInfo) : base(null) { - this.beatmap = beatmap; + this.beatmapInfo = beatmapInfo; } [BackgroundDependencyLoader] @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Select.Carousel { if (weakScore.NewValue.TryGetTarget(out var score)) { - if (score.BeatmapInfoID == beatmap.ID) + if (score.BeatmapInfoID == beatmapInfo.ID) fetchAndLoadTopScore(); } } @@ -79,10 +79,10 @@ namespace osu.Game.Screens.Select.Carousel private ScoreInfo fetchTopScore() { - if (scores == null || beatmap == null || ruleset?.Value == null || api?.LocalUser.Value == null) + if (scores == null || beatmapInfo == null || ruleset?.Value == null || api?.LocalUser.Value == null) return null; - return scores.QueryScores(s => s.UserID == api.LocalUser.Value.Id && s.BeatmapInfoID == beatmap.ID && s.RulesetID == ruleset.Value.ID && !s.DeletePending) + return scores.QueryScores(s => s.UserID == api.LocalUser.Value.Id && s.BeatmapInfoID == beatmapInfo.ID && s.RulesetID == ruleset.Value.ID && !s.DeletePending) .OrderByDescending(s => s.TotalScore) .FirstOrDefault(); } diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 7820264505..2fdb41a1a1 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -25,17 +25,17 @@ namespace osu.Game.Screens.Select.Leaderboards [Resolved] private RulesetStore rulesets { get; set; } - private BeatmapInfo beatmap; + private BeatmapInfo beatmapInfo; - public BeatmapInfo Beatmap + public BeatmapInfo BeatmapInfo { - get => beatmap; + get => beatmapInfo; set { - if (beatmap == value) + if (beatmapInfo == value) return; - beatmap = value; + beatmapInfo = value; Scores = null; UpdateScores(); @@ -116,7 +116,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (score.NewValue.TryGetTarget(out var scoreInfo)) { - if (Beatmap?.ID != scoreInfo.BeatmapInfoID) + if (BeatmapInfo?.ID != scoreInfo.BeatmapInfoID) return; } @@ -132,7 +132,7 @@ namespace osu.Game.Screens.Select.Leaderboards loadCancellationSource?.Cancel(); loadCancellationSource = new CancellationTokenSource(); - if (Beatmap == null) + if (BeatmapInfo == null) { PlaceholderState = PlaceholderState.NoneSelected; return null; @@ -141,7 +141,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (Scope == BeatmapLeaderboardScope.Local) { var scores = scoreManager - .QueryScores(s => !s.DeletePending && s.Beatmap.ID == Beatmap.ID && s.Ruleset.ID == ruleset.Value.ID); + .QueryScores(s => !s.DeletePending && s.Beatmap.ID == BeatmapInfo.ID && s.Ruleset.ID == ruleset.Value.ID); if (filterMods && !mods.Value.Any()) { @@ -168,7 +168,7 @@ namespace osu.Game.Screens.Select.Leaderboards return null; } - if (Beatmap.OnlineBeatmapID == null || Beatmap?.Status <= BeatmapSetOnlineStatus.Pending) + if (BeatmapInfo.OnlineBeatmapID == null || BeatmapInfo?.Status <= BeatmapSetOnlineStatus.Pending) { PlaceholderState = PlaceholderState.Unavailable; return null; @@ -188,7 +188,7 @@ namespace osu.Game.Screens.Select.Leaderboards else if (filterMods) requestMods = mods.Value; - var req = new GetScoresRequest(Beatmap, ruleset.Value ?? Beatmap.Ruleset, Scope, requestMods); + var req = new GetScoresRequest(BeatmapInfo, ruleset.Value ?? BeatmapInfo.Ruleset, Scope, requestMods); req.Success += r => { diff --git a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs index 085ea372c0..1ae244281b 100644 --- a/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs +++ b/osu.Game/Screens/Select/LocalScoreDeleteDialog.cs @@ -29,8 +29,8 @@ namespace osu.Game.Screens.Select [BackgroundDependencyLoader] private void load() { - BeatmapInfo beatmap = beatmapManager.QueryBeatmap(b => b.ID == score.BeatmapInfoID); - Debug.Assert(beatmap != null); + BeatmapInfo beatmapInfo = beatmapManager.QueryBeatmap(b => b.ID == score.BeatmapInfoID); + Debug.Assert(beatmapInfo != null); BodyText = $"{score.User} ({score.DisplayAccuracy}, {score.Rank})"; diff --git a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs index c87a4bbc54..b8b8e3e4bc 100644 --- a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.Select { base.Beatmap = value; - Leaderboard.Beatmap = value is DummyWorkingBeatmap ? null : value?.BeatmapInfo; + Leaderboard.BeatmapInfo = value is DummyWorkingBeatmap ? null : value?.BeatmapInfo; } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index e4ab360765..6cafcb9d16 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -345,22 +345,22 @@ namespace osu.Game.Screens.Select /// protected abstract BeatmapDetailArea CreateBeatmapDetailArea(); - public void Edit(BeatmapInfo beatmap = null) + public void Edit(BeatmapInfo beatmapInfo = null) { if (!AllowEditing) throw new InvalidOperationException($"Attempted to edit when {nameof(AllowEditing)} is disabled"); - Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap ?? beatmapNoDebounce); + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo ?? beatmapInfoNoDebounce); this.Push(new EditorLoader()); } /// /// Call to make a selection and perform the default action for this SongSelect. /// - /// An optional beatmap to override the current carousel selection. + /// An optional beatmap to override the current carousel selection. /// An optional ruleset to override the current carousel selection. /// An optional custom action to perform instead of . - public void FinaliseSelection(BeatmapInfo beatmap = null, RulesetInfo ruleset = null, Action customStartAction = null) + public void FinaliseSelection(BeatmapInfo beatmapInfo = null, RulesetInfo ruleset = null, Action customStartAction = null) { // This is very important as we have not yet bound to screen-level bindables before the carousel load is completed. if (!Carousel.BeatmapSetsLoaded) @@ -379,8 +379,8 @@ namespace osu.Game.Screens.Select // this could happen via a user interaction while the carousel is still in a loading state. if (Carousel.SelectedBeatmapInfo == null) return; - if (beatmap != null) - Carousel.SelectBeatmap(beatmap); + if (beatmapInfo != null) + Carousel.SelectBeatmap(beatmapInfo); if (selectionChangedDebounce?.Completed == false) { @@ -435,18 +435,18 @@ namespace osu.Game.Screens.Select } // We need to keep track of the last selected beatmap ignoring debounce to play the correct selection sounds. - private BeatmapInfo beatmapNoDebounce; + private BeatmapInfo beatmapInfoNoDebounce; private RulesetInfo rulesetNoDebounce; - private void updateSelectedBeatmap(BeatmapInfo beatmap) + private void updateSelectedBeatmap(BeatmapInfo beatmapInfo) { - if (beatmap == null && beatmapNoDebounce == null) + if (beatmapInfo == null && beatmapInfoNoDebounce == null) return; - if (beatmap?.Equals(beatmapNoDebounce) == true) + if (beatmapInfo?.Equals(beatmapInfoNoDebounce) == true) return; - beatmapNoDebounce = beatmap; + beatmapInfoNoDebounce = beatmapInfo; performUpdateSelected(); } @@ -467,12 +467,12 @@ namespace osu.Game.Screens.Select /// private void performUpdateSelected() { - var beatmap = beatmapNoDebounce; + var beatmap = beatmapInfoNoDebounce; var ruleset = rulesetNoDebounce; selectionChangedDebounce?.Cancel(); - if (beatmapNoDebounce == null) + if (beatmapInfoNoDebounce == null) run(); else selectionChangedDebounce = Scheduler.AddDelayed(run, 200); @@ -803,11 +803,11 @@ namespace osu.Game.Screens.Select dialogOverlay?.Push(new BeatmapDeleteDialog(beatmap)); } - private void clearScores(BeatmapInfo beatmap) + private void clearScores(BeatmapInfo beatmapInfo) { - if (beatmap == null || beatmap.ID <= 0) return; + if (beatmapInfo == null || beatmapInfo.ID <= 0) return; - dialogOverlay?.Push(new BeatmapClearScoresDialog(beatmap, () => + dialogOverlay?.Push(new BeatmapClearScoresDialog(beatmapInfo, () => // schedule done here rather than inside the dialog as the dialog may fade out and never callback. Schedule(() => BeatmapDetails.Refresh()))); } diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index e6ddeba316..2093182dcc 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -20,8 +20,8 @@ namespace osu.Game.Skinning protected override bool AllowManiaSkin => false; protected override bool UseCustomSampleBanks => true; - public LegacyBeatmapSkin(BeatmapInfo beatmap, IResourceStore storage, IStorageResourceProvider resources) - : base(createSkinInfo(beatmap), new LegacySkinResourceStore(beatmap.BeatmapSet, storage), resources, beatmap.Path) + public LegacyBeatmapSkin(BeatmapInfo beatmapInfo, IResourceStore storage, IStorageResourceProvider resources) + : base(createSkinInfo(beatmapInfo), new LegacySkinResourceStore(beatmapInfo.BeatmapSet, storage), resources, beatmapInfo.Path) { // Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer) Configuration.AllowDefaultComboColoursFallback = false; @@ -76,7 +76,7 @@ namespace osu.Game.Skinning return base.GetSample(sampleInfo); } - private static SkinInfo createSkinInfo(BeatmapInfo beatmap) => - new SkinInfo { Name = beatmap.ToString(), Creator = beatmap.Metadata?.AuthorString }; + private static SkinInfo createSkinInfo(BeatmapInfo beatmapInfo) => + new SkinInfo { Name = beatmapInfo.ToString(), Creator = beatmapInfo.Metadata?.AuthorString }; } } diff --git a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs index 27162b1d66..5c522058d9 100644 --- a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs @@ -111,8 +111,8 @@ namespace osu.Game.Tests.Beatmaps public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.DarkGoldenrod; - public TestBeatmapSkin(BeatmapInfo beatmap, bool hasColours) - : base(beatmap, new ResourceStore(), null) + public TestBeatmapSkin(BeatmapInfo beatmapInfo, bool hasColours) + : base(beatmapInfo, new ResourceStore(), null) { if (hasColours) { diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 75aa4866ff..91bcb37fcc 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -27,13 +27,13 @@ namespace osu.Game.Users public abstract class InGame : UserActivity { - public BeatmapInfo Beatmap { get; } + public BeatmapInfo BeatmapInfo { get; } public RulesetInfo Ruleset { get; } - protected InGame(BeatmapInfo info, RulesetInfo ruleset) + protected InGame(BeatmapInfo beatmapInfo, RulesetInfo ruleset) { - Beatmap = info; + BeatmapInfo = beatmapInfo; Ruleset = ruleset; } @@ -42,8 +42,8 @@ namespace osu.Game.Users public class InMultiplayerGame : InGame { - public InMultiplayerGame(BeatmapInfo beatmap, RulesetInfo ruleset) - : base(beatmap, ruleset) + public InMultiplayerGame(BeatmapInfo beatmapInfo, RulesetInfo ruleset) + : base(beatmapInfo, ruleset) { } @@ -52,27 +52,27 @@ namespace osu.Game.Users public class InPlaylistGame : InGame { - public InPlaylistGame(BeatmapInfo beatmap, RulesetInfo ruleset) - : base(beatmap, ruleset) + public InPlaylistGame(BeatmapInfo beatmapInfo, RulesetInfo ruleset) + : base(beatmapInfo, ruleset) { } } public class InSoloGame : InGame { - public InSoloGame(BeatmapInfo info, RulesetInfo ruleset) - : base(info, ruleset) + public InSoloGame(BeatmapInfo beatmapInfo, RulesetInfo ruleset) + : base(beatmapInfo, ruleset) { } } public class Editing : UserActivity { - public BeatmapInfo Beatmap { get; } + public BeatmapInfo BeatmapInfo { get; } public Editing(BeatmapInfo info) { - Beatmap = info; + BeatmapInfo = info; } public override string Status => @"Editing a beatmap"; From 281a3a0cea270b1531cc13ee1a9ae6cb559788c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 2 Oct 2021 18:40:41 +0200 Subject: [PATCH 157/391] Add test case for legacy loop count behaviour --- .../Formats/LegacyStoryboardDecoderTest.cs | 27 +++++++++++++++++++ osu.Game.Tests/Resources/loop-count.osb | 15 +++++++++++ 2 files changed, 42 insertions(+) create mode 100644 osu.Game.Tests/Resources/loop-count.osb diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index bcde899789..560e2ef894 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -149,5 +149,32 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(AnimationLoopType.LoopForever, ((StoryboardAnimation)foreground.Elements[5]).LoopType); } } + + [Test] + public void TestDecodeLoopCount() + { + // all loop sequences in loop-count.osb have a total duration of 2000ms (fade in 0->1000ms, fade out 1000->2000ms). + const double loop_duration = 2000; + + var decoder = new LegacyStoryboardDecoder(); + + using (var resStream = TestResources.OpenResource("loop-count.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3); + + // stable ensures that any loop command executes at least once, even if the loop count specified in the .osb is zero or negative. + StoryboardSprite zeroTimes = background.Elements.OfType().Single(s => s.Path == "zero-times.png"); + Assert.That(zeroTimes.EndTime, Is.EqualTo(1000 + loop_duration)); + + StoryboardSprite oneTime = background.Elements.OfType().Single(s => s.Path == "one-time.png"); + Assert.That(oneTime.EndTime, Is.EqualTo(4000 + loop_duration)); + + StoryboardSprite manyTimes = background.Elements.OfType().Single(s => s.Path == "many-times.png"); + Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + 40 * loop_duration)); + } + } } } diff --git a/osu.Game.Tests/Resources/loop-count.osb b/osu.Game.Tests/Resources/loop-count.osb new file mode 100644 index 0000000000..ec75e85ef1 --- /dev/null +++ b/osu.Game.Tests/Resources/loop-count.osb @@ -0,0 +1,15 @@ +osu file format v14 + +[Events] +Sprite,Background,TopCentre,"zero-times.png",320,240 + L,1000,0 + F,0,0,1000,0,1 + F,0,1000,2000,1,0 +Sprite,Background,TopCentre,"one-time.png",320,240 + L,4000,1 + F,0,0,1000,0,1 + F,0,1000,2000,1,0 +Sprite,Background,TopCentre,"many-times.png",320,240 + L,9000,40 + F,0,0,1000,0,1 + F,0,1000,2000,1,0 From 3e403cfe031604792798898218927691d3c2fe21 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Sat, 2 Oct 2021 19:16:46 +0200 Subject: [PATCH 158/391] Add comment explaining the purpose of the empty `FilterTerms` --- .../Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index 806390c0ec..2cc2857e9b 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -76,6 +76,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input Content.CornerRadius = 5; } + // Empty FilterTerms so that the ResetButton is visible only when the whole subsection is visible. public override IEnumerable FilterTerms => Enumerable.Empty(); } } From bc1ff019da91aebf64f730b424ba4f442096ef71 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Sun, 3 Oct 2021 11:27:17 +0100 Subject: [PATCH 159/391] Implement relax checks --- .../Difficulty/OsuPerformanceCalculator.cs | 15 +++++++++++++++ osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index bf4d92652c..a8e43ce09b 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -90,6 +90,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= lengthBonus; + if (mods.Any(h => h is OsuModRelax)) + { + aimValue *= 0.75; + countMiss += countOk + countMeh; + } + // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (countMiss > 0) aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss); @@ -126,6 +132,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; + if (mods.Any(h => h is OsuModRelax)) + { + speedValue *= 0.75; + countMiss += countOk + countMeh; + } + // Longer maps are worth more. double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); @@ -160,6 +172,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeAccuracyValue() { + if (mods.Any(h => h is OsuModRelax)) + return 0.0; + // 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; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 9364b11048..e8169ba660 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -7,6 +7,8 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; using osu.Framework.Utils; +using System.Linq; +using osu.Game.Rulesets.Osu.Mods; namespace osu.Game.Rulesets.Osu.Difficulty.Skills { @@ -79,6 +81,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills } } + if (Mods.Any(m => m is OsuModRelax)) + speedBonus = 0.0; + return (1 + (speedBonus - 1) * 0.75) * angleBonus * (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) From 03f0a3658947d0b99e0d91c7e06f0700e728f08f Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Sun, 3 Oct 2021 11:53:14 +0100 Subject: [PATCH 160/391] additional balancing --- .../Difficulty/OsuPerformanceCalculator.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index a8e43ce09b..62e0dea4fa 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -40,6 +40,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); + if (mods.Any(h => h is OsuModRelax)) + { + countMiss += countOk + countMeh; + } + // Custom multipliers for NoFail and SpunOut. double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. @@ -92,8 +97,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (mods.Any(h => h is OsuModRelax)) { - aimValue *= 0.75; - countMiss += countOk + countMeh; + aimValue *= 0.6; } // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. @@ -134,8 +138,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (mods.Any(h => h is OsuModRelax)) { - speedValue *= 0.75; - countMiss += countOk + countMeh; + speedValue *= 0.6; } // Longer maps are worth more. @@ -219,6 +222,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (mods.Any(h => h is OsuModHidden)) flashlightValue *= 1.3; + if (mods.Any(h => h is OsuModRelax)) + { + flashlightValue *= 0.6; + } + // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (countMiss > 0) flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875)); From f05cb6bb5b677255517212369ed4292d1d4c48e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 3 Oct 2021 13:53:26 +0200 Subject: [PATCH 161/391] Add test case covering reset section button hiding --- .../Visual/Settings/TestSceneKeyBindingPanel.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index 168d9fafcf..1effe52608 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Settings.Sections.Input; using osuTK.Input; @@ -230,6 +231,22 @@ namespace osu.Game.Tests.Visual.Settings AddAssert("first binding selected", () => multiBindingRow.ChildrenOfType().First().IsBinding); } + [Test] + public void TestFilteringHidesResetSectionButtons() + { + SearchTextBox searchTextBox = null; + + AddStep("add any search term", () => + { + searchTextBox = panel.ChildrenOfType().Single(); + searchTextBox.Current.Value = "chat"; + }); + AddUntilStep("all reset section bindings buttons hidden", () => panel.ChildrenOfType().All(button => button.Alpha == 0)); + + AddStep("clear search term", () => searchTextBox.Current.Value = string.Empty); + AddUntilStep("all reset section bindings buttons shown", () => panel.ChildrenOfType().All(button => button.Alpha == 1)); + } + private void checkBinding(string name, string keyName) { AddAssert($"Check {name} is bound to {keyName}", () => From 4f00a9e165af5d7b8a321e2bf72fe81144331482 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 Oct 2021 22:32:46 +0900 Subject: [PATCH 162/391] Adjust max runtime for diffcalc runs --- .github/workflows/diffcalc.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index bc2626d3d6..9e11ab6663 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -53,6 +53,7 @@ jobs: diffcalc: name: Run runs-on: self-hosted + timeout-minutes: 1440 if: needs.metadata.outputs.continue == 'yes' needs: metadata strategy: From 07c11953cddbf66b5b84c996449f6af82821b1d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 3 Oct 2021 15:39:54 +0200 Subject: [PATCH 163/391] Modify special test skin to visually cover regression --- .../special-skin/hitcircleoverlay@2x.png | Bin 247101 -> 26595 bytes .../Resources/special-skin/skin.ini | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/special-skin/skin.ini diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/special-skin/hitcircleoverlay@2x.png index a9b2d95d882b99a53aeba501919b5e5f1600bf26..8e50cd033596fb3daacd173247d6779168cc8f99 100755 GIT binary patch literal 26595 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

yftJ!Y0GsdcN%;MEM+5u>2KjHnOqx_AeVsl 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 literal 247101 zcmb^4Ys|j+b{F(1^-wC*dWfKvbR3L8;MsGX4`VlQ9ZMT_*^~k;V&FcuWA~mJW@fj$ z4W><05G4>WM0`Pz7>yO>of6WRLIfmYh@ymGOfbIjg$a!!ZzLW*>wcd9Zee~G+|Tn| zo3i&D{?~P1ztdXZ^mw|@S2{F?c@-+Jro{N>lZ^$q`! z|M{&S|HoH<)4o4VUmVUK9xrcy_qUI4|I)ngyI=O!Tfg)V{Gs3fQ{VYtees|C-9Pm$ z%b)vWzv&zP*Z<|CU-Ij}_@%${XMXQ@{7e7V+u!@gr$6|geaG~Nj{o=vf8xjf?{EHn zAN-=<`al1xKj)Wx`=1&9kDvQHzy8nt#J@KE>p$-w{I~!5|Mf@ydjHSq#KL4%1xBt^W@8jS0 zb-(r({mVc2eLwv@fAK%LeBWRG8~?)a&;R~^_}}||f2sUa-}<|M&v*V4 zf91db^}paR{PfrV_WWDF)c&GB^_%|l|Mb87TR-;ye*Pc(tAF$I!5{thZ}`=};`6`Y z`-ZRnvCsY3Z~TS-;=l91AOGu*{^@V}>BEnH&-o|YkN)J>{Ud*J`7eIUzwv+mQ{VPY zKlaw2{`0@?|M{E0;aB|yGx>8r`ZeG8$y;xI)1Qt1|8;-pmw(f@y!G`z`a9nLp6~ti zd%y5&-#dNy<w8W=`h%zb{LZiXm2ZFBSKsr3FJC@Ay#3WL zf9Zoy-uvq3-?_c_J%1hl_}zEjzWtF;fAI6~JTB<>e&LI6Pal4KdAt5x^KiL>yd7^m96$WZr=Nevmp%^R%O8F1wI6))G)*S#-LD=#dN==E7DKxI!Sm^Bf9|8N zeEg-`kDpKPUcPks^5uh1Kk;Ytul?DNe&EATKm6neKHPZq*Z%e2@dKvz3t#x!ZzR9v z%U}N5Pki#}@lbXk7LXa3?hqxt01@4o!ZeBSZ6 zKmN6cKYjnY@ofB?zhh9BufD(WN8{go?T>!+-t_V1@acyif6s>>e(BBL`Qn>de0!SS z{-wY9{SVF`{_rQiEJpD3$1n5Y)63`IiEZ?dwGT!1J$d$CR=-y@zdS47%d*Em`h^eA z?_Yo9Yk#zlKU(^u{d-yeUXlHB|NXT;Y7d_epC10cfB1ZQ@A~1#Up`oY_rHAj{>!@` zeenJ7y#2LxeQ)~kOCNshhkaf7v;X;buyh}Pczyp%{>9&I@or!Hg)hAK9Upx1>EVOZ zNClp)by-roLRSP5C%l&(9y8z7kt9 zo_{=@=MPWb@iSl7wN=@k^8BI8y5qy8sM?3)p{X9KLwRbi*{RKo^P8{vbsxXaKpejG zv)>wDb&jt(cX?Mfmxoi|W)Ih_>K?kbygW2{-(IhIpI@@mJ8#FHc+YEm(jiXa=ikYn z{^9q=-u%*69{*wAWd8GR9{>IH@4xTw^Y4G=U;oAr-nSIRJ8#FXdYt{=z`&X1rmoBS z>7Re`{jXlW^u6=@X6KXJhZJ@5v;Woi{aycx4f>f$douxprTN58{p86)jfd}l`l~O! z|0}bs$cwx$%dTyQVra{Cn3t@n#zdD1wk-YFP4zIf zc|OjmCzhe9iaH;1;~B;xYqPOf%YGc@uI!4$6GdKT<&-%b5>%!~WM zA)ia%xh8&N=*qgvhM3W&>5IDVy`WyEX&Kvm>Jv|lPNSDwjB2fxX~~wd&7F{bs;hcf zQcq0zu=L*CEagqMBHS!a?fud93oR8S~Wa`l6~UvbmTG=Y3lH*^bJqzRcQkDN?(A zUB)n0^*rU(z}iiHQPwQw)D%tGB}UE%EOW_b)&8_EtIMpZ=PGa4`0*uiKJv_N&zi=$ z7}h467`t|==ej7nd2y~jyGwFbdY;yzs@lG8hmu7s%VntAa%l6cO?_29jmuK-a&y%$ zE&EuH*}6KA{z;!F>{VVg?NZnN^i)rC*M?KDSH{*4GQX*D6x}klQ&x@B%7}Jeo7Z#J zteuh7bz(j8g6%2FWv$1$XAg^cwxzn!?{u>RlNfnf5AD#-w(exmc6zp~%`jJtqT?~9 zo*2WŜ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

>Yvp|~X6yDxUT?flxj)t$a%;QQ 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�R>aQjB9oI!EpJo00!>LKYVJ-AR(0vZg zg(M;+D_&!VKQ61dJ8`r%>#-JP%tglYrkj=v#g(@o}~;adv+E4 zg)sKLsQNd>h3)K6oM)_4gawM6N87$mj529pb`4~XgfNnj8H@FkH^s{wx`kZ6iK$?O zs>qL(-!S Date: Sun, 3 Oct 2021 15:40:32 +0200 Subject: [PATCH 164/391] Proxy slider head circle number along with overlay --- .../Skinning/Legacy/LegacyMainCirclePiece.cs | 22 ++++++++++++------- .../Legacy/LegacySliderHeadHitCircle.cs | 8 +++---- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index 3afd814174..8b45513a2e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -35,8 +35,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private Drawable hitCircleSprite; - protected Drawable HitCircleOverlay { get; private set; } + protected Container OverlayLayer; + private Drawable hitCircleOverlay; private SkinnableSpriteText hitCircleText; private readonly Bindable accentColour = new Bindable(); @@ -78,17 +79,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - HitCircleOverlay = new KiaiFlashingSprite + OverlayLayer = new Container { - Texture = overlayTexture, Anchor = Anchor.Centre, Origin = Anchor.Centre, - }, + Child = hitCircleOverlay = new KiaiFlashingSprite + { + Texture = overlayTexture, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } }; if (hasNumber) { - AddInternal(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText + OverlayLayer.Add(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText { Font = OsuFont.Numeric.With(size: 40), UseFullGlyphHeight = false, @@ -102,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; if (overlayAboveNumber) - ChangeInternalChildDepth(HitCircleOverlay, float.MinValue); + OverlayLayer.ChangeChildDepth(hitCircleOverlay, float.MinValue); accentColour.BindTo(drawableObject.AccentColour); indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); @@ -147,8 +153,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy hitCircleSprite.FadeOut(legacy_fade_duration, Easing.Out); hitCircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); - HitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out); - HitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + hitCircleOverlay.FadeOut(legacy_fade_duration, Easing.Out); + hitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); if (hasNumber) { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderHeadHitCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderHeadHitCircle.cs index 13ba42ba50..7de2b8c7fa 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderHeadHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderHeadHitCircle.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy [Resolved(canBeNull: true)] private DrawableHitObject drawableHitObject { get; set; } - private Drawable proxiedHitCircleOverlay; + private Drawable proxiedOverlayLayer; public LegacySliderHeadHitCircle() : base("sliderstartcircle") @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy protected override void LoadComplete() { base.LoadComplete(); - proxiedHitCircleOverlay = HitCircleOverlay.CreateProxy(); + proxiedOverlayLayer = OverlayLayer.CreateProxy(); if (drawableHitObject != null) { @@ -35,11 +35,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private void onHitObjectApplied(DrawableHitObject drawableObject) { - Debug.Assert(proxiedHitCircleOverlay.Parent == null); + Debug.Assert(proxiedOverlayLayer.Parent == null); // see logic in LegacyReverseArrow. (drawableObject as DrawableSliderHead)?.DrawableSlider - .OverlayElementContainer.Add(proxiedHitCircleOverlay.With(d => d.Depth = float.MinValue)); + .OverlayElementContainer.Add(proxiedOverlayLayer.With(d => d.Depth = float.MinValue)); } protected override void Dispose(bool isDisposing) From 5e5cdaab5ef1a931541fe76941a4770624b0eed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 3 Oct 2021 19:14:01 +0200 Subject: [PATCH 165/391] Privatise setter Co-authored-by: Dean Herbert --- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index 8b45513a2e..d1c9b1bf92 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private Drawable hitCircleSprite; - protected Container OverlayLayer; + protected Container OverlayLayer { get; private set; } private Drawable hitCircleOverlay; private SkinnableSpriteText hitCircleText; From ba2c44a2f4325d567b2b79f8d04b7a61e295e822 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Sun, 3 Oct 2021 04:35:37 +0000 Subject: [PATCH 166/391] reworked strain to fix issue with overlapping hitwindo --- .../Difficulty/Skills/Speed.cs | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 6b21ae7ac6..2193d503ea 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 const double single_spacing_threshold = 125; - private const double rhythm_multiplier = 4.0; + private const double rhythm_multiplier = 0.675; private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max. private const double min_speed_bonus = 75; // ~200BPM private const double max_speed_bonus = 45; @@ -44,14 +44,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills ///

/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current . /// - private double calculateRhythmBonus(DifficultyHitObject current, double greatWindowFull) + private double calculateRhythmBonus(DifficultyHitObject current) { if (current.BaseObject is Spinner) return 0; - int previousIslandSize = -1; + int previousIslandSize = 0; + double rhythmComplexitySum = 0; - int islandSize = 0; + int islandSize = 1; double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms bool firstDeltaSwitch = false; @@ -66,71 +67,67 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (currHistoricalDecay != 0) { - currHistoricalDecay = Math.Min(currHistoricalDecay, (double)(Previous.Count - i) / Previous.Count); // either we're limited by time or limited by object count. + currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count. double currDelta = Math.Max(25, currObj.DeltaTime); double prevDelta = Math.Max(25, prevObj.DeltaTime); double lastDelta = ((OsuDifficultyHitObject)lastObj).StrainTime; - double effectiveRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta); + double currRatio = 1.0 + Math.Min(4.5, 6 * Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses. - if (effectiveRatio > 0.5) - effectiveRatio = 0.5 + (effectiveRatio - 0.5) * 6; // large buff for 1/3 -> 1/4 type transitions. - else - effectiveRatio = 0.5; + double windowPenalty = Math.Min(1, Math.Max(0, Math.Max(prevDelta, currDelta) - Math.Min(prevDelta, currDelta) - greatWindow) / greatWindow); - effectiveRatio *= currHistoricalDecay; // scale with time + windowPenalty = Math.Min(1, windowPenalty * (previousIslandSize + islandSize)); if (firstDeltaSwitch) { - if (Precision.AlmostEquals(prevDelta, currDelta, 15)) + if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta)) { islandSize++; // island is still progressing, count size. } else { - if (islandSize > 12) - islandSize = 12; + double effectiveRatio = windowPenalty * currRatio; + + if (islandSize > 7) + islandSize = 7; if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window - effectiveRatio *= 0.25; + effectiveRatio *= 0.125; if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle - effectiveRatio *= 0.5; + effectiveRatio *= 0.25; if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet) - effectiveRatio *= 0.35; + effectiveRatio *= 0.25; if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5) - effectiveRatio *= 0.75; + 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. effectiveRatio *= 0.125; - rhythmComplexitySum += effectiveRatio * startRatio; + rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2; - startRatio = Math.Sqrt(Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta)); + startRatio = windowPenalty * currRatio; previousIslandSize = islandSize; // log the last island size. if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size. - islandSize = 0; + islandSize = 1; } } else if (prevDelta > 1.25 * currDelta) // we want to be speeding up. { // Begin counting island until we change speed again. firstDeltaSwitch = true; - islandSize = 0; - startRatio = Math.Sqrt(Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta)); + startRatio = windowPenalty * currRatio; + islandSize = 1; } } } - if (greatWindowFull > 62) - rhythmComplexitySum *= Math.Sqrt(62 / greatWindowFull); - return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though) } @@ -156,7 +153,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); - protected override double CalculateInitialStrain(double time) => (currentMovementStrain + currentTapStrain * currentRhythm) * strainDecay(time - Previous[0].StartTime); + protected override double CalculateInitialStrain(double time) => ((currentMovementStrain + currentTapStrain) * currentRhythm) * strainDecay(time - Previous[0].StartTime); protected override double StrainValueAt(DifficultyHitObject current) { @@ -182,9 +179,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (strainTime < min_speed_bonus) speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); - currentRhythm = calculateRhythmBonus(current, greatWindowFull); + currentRhythm = calculateRhythmBonus(current); double decay = strainDecay(current.DeltaTime); + double tapStrain = tapStrainOf(current, speedBonus, strainTime) * skillMultiplier; + double maxStrain = (1 / decay) * tapStrain; currentTapStrain *= decay; currentTapStrain += tapStrainOf(current, speedBonus, strainTime) * skillMultiplier; @@ -192,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills currentMovementStrain *= decay; currentMovementStrain += movementStrainOf(current, speedBonus, strainTime) * skillMultiplier; - return currentMovementStrain + currentTapStrain * currentRhythm; + return (currentMovementStrain + currentTapStrain) * currentRhythm; } } } From 6d134b2a83e4fde69cfae550d56352550c64c7a5 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Sun, 3 Oct 2021 17:06:59 +0000 Subject: [PATCH 167/391] resolved code cleanliness issues --- .../Difficulty/Skills/Speed.cs | 71 +++++++------------ 1 file changed, 25 insertions(+), 46 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 2193d503ea..4048d84d35 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -19,14 +19,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private const double rhythm_multiplier = 0.675; private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max. private const double min_speed_bonus = 75; // ~200BPM - private const double max_speed_bonus = 45; private const double speed_balancing_factor = 40; private double skillMultiplier => 1375; private double strainDecayBase => 0.3; - private double currentTapStrain = 1; - private double currentMovementStrain = 1; + private double currentStrain = 1; private double currentRhythm = 1; protected override int ReducedSectionCount => 5; @@ -59,9 +57,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills for (int i = Previous.Count - 2; i > 0; i--) { - DifficultyHitObject currObj = Previous[i - 1]; - DifficultyHitObject prevObj = Previous[i]; - DifficultyHitObject lastObj = Previous[i + 1]; + OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)Previous[i - 1]; + OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)Previous[i]; + OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)Previous[i + 1]; double currHistoricalDecay = Math.Max(0, (history_time_max - (current.StartTime - currObj.StartTime))) / history_time_max; // scales note 0 to 1 from history to now @@ -69,9 +67,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count. - double currDelta = Math.Max(25, currObj.DeltaTime); - double prevDelta = Math.Max(25, prevObj.DeltaTime); - double lastDelta = ((OsuDifficultyHitObject)lastObj).StrainTime; + double currDelta = currObj.StrainTime; + double prevDelta = prevObj.StrainTime; + double lastDelta = lastObj.StrainTime; double currRatio = 1.0 + Math.Min(4.5, 6 * 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.Max(prevDelta, currDelta) - Math.Min(prevDelta, currDelta) - greatWindow) / greatWindow); @@ -82,15 +80,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta)) { - islandSize++; // island is still progressing, count size. + if (islandSize < 7) + islandSize++; // island is still progressing, count size. } else { double effectiveRatio = windowPenalty * currRatio; - if (islandSize > 7) - islandSize = 7; - if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window effectiveRatio *= 0.125; @@ -131,32 +127,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though) } - private double tapStrainOf(DifficultyHitObject current, double speedBonus, double strainTime) + private double strainValueOf(DifficultyHitObject current) { if (current.BaseObject is Spinner) return 0; - return speedBonus / strainTime; - } - - private double movementStrainOf(DifficultyHitObject current, double speedBonus, double strainTime) - { - if (current.BaseObject is Spinner) - return 0; - - var osuCurrObj = (OsuDifficultyHitObject)current; - - double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.JumpDistance); - - return (speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime; - } - - private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); - - protected override double CalculateInitialStrain(double time) => ((currentMovementStrain + currentTapStrain) * currentRhythm) * strainDecay(time - Previous[0].StartTime); - - protected override double StrainValueAt(DifficultyHitObject current) - { // derive strainTime for calculation var osuCurrObj = (OsuDifficultyHitObject)current; var osuPrevObj = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null; @@ -179,19 +154,23 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills if (strainTime < min_speed_bonus) speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); + double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.JumpDistance); + + return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime; + } + + private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); + + protected override double CalculateInitialStrain(double time) => (currentStrain * currentRhythm) * strainDecay(time - Previous[0].StartTime); + + protected override double StrainValueAt(DifficultyHitObject current) + { + currentStrain *= strainDecay(current.DeltaTime); + currentStrain += strainValueOf(current) * skillMultiplier; + currentRhythm = calculateRhythmBonus(current); - double decay = strainDecay(current.DeltaTime); - double tapStrain = tapStrainOf(current, speedBonus, strainTime) * skillMultiplier; - double maxStrain = (1 / decay) * tapStrain; - - currentTapStrain *= decay; - currentTapStrain += tapStrainOf(current, speedBonus, strainTime) * skillMultiplier; - - currentMovementStrain *= decay; - currentMovementStrain += movementStrainOf(current, speedBonus, strainTime) * skillMultiplier; - - return (currentMovementStrain + currentTapStrain) * currentRhythm; + return currentStrain * currentRhythm; } } } From bc3ae4c4f839d9cc9693259953db570d68bc9025 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Sun, 3 Oct 2021 17:36:34 +0000 Subject: [PATCH 168/391] changed function names to be consistent --- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 6 ++---- osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs | 4 +--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 93df6e90f8..d8f4aa1229 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private double skillMultiplier => 26.25; private double strainDecayBase => 0.15; - private double aimStrainOf(DifficultyHitObject current) + private double strainValueOf(DifficultyHitObject current) { if (current.BaseObject is Spinner) return 0; @@ -69,10 +69,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills protected override double StrainValueAt(DifficultyHitObject current) { - double aimStrain = aimStrainOf(current); - currentStrain *= strainDecay(current.DeltaTime); - currentStrain += aimStrain * skillMultiplier; + currentStrain += strainValueOf(current) * skillMultiplier; return currentStrain; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs index 7121582da7..e3abe7d700 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Flashlight.cs @@ -70,10 +70,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills protected override double StrainValueAt(DifficultyHitObject current) { - double flashlightStrain = strainValueOf(current); - currentStrain *= strainDecay(current.DeltaTime); - currentStrain += flashlightStrain * skillMultiplier; + currentStrain += strainValueOf(current) * skillMultiplier; return currentStrain; } From 94f8692b007fe0912bdf5d0b5c5a14a862923e7e Mon Sep 17 00:00:00 2001 From: Xexxar Date: Sun, 3 Oct 2021 17:42:49 +0000 Subject: [PATCH 169/391] removed acc changes to put in seperate PR --- .../Difficulty/OsuPerformanceCalculator.cs | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 87aecd32f7..bf4d92652c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -160,28 +160,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double computeAccuracyValue() { + // 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; - // This section should be documented by Tr3, but effectively we're calculating the exact same way as before, but - // we calculate a variance based on the object count and # of 50s, 100s, etc. This prevents us from having cases - // where an SS on lower OD is actually worth more than a 95% on OD11, even though the OD11 requires a greater - // window of precision. + if (amountHitObjectsWithAccuracy > 0) + betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); + else + betterAccuracyPercentage = 0; - double p100 = (2 * (double)countOk) / amountHitObjectsWithAccuracy; // this is multiplied by two to encourage better accuracy. (scales better) - double p50 = (1 * (double)countMeh) / amountHitObjectsWithAccuracy; - double pm = (1 * (double)countMiss) / amountHitObjectsWithAccuracy; - double p300 = Math.Max(0, 1.0 - pm - p100 - p50); + // It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points. + if (betterAccuracyPercentage < 0) + betterAccuracyPercentage = 0; - double m300 = 79.5 - 6.0 * Attributes.OverallDifficulty; - double m100 = 139.5 - 8.0 * Attributes.OverallDifficulty; - double m50 = 199.5 - 10.0 * Attributes.OverallDifficulty; + // Lots of arbitrary values from testing. + // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution. + double accuracyValue = Math.Pow(1.52163, Attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83; - double variance = p300 * Math.Pow(m300 / 2.0, 2.0) + - p100 * Math.Pow((m300 + m100) / 2.0, 2.0) + - p50 * Math.Pow((m100 + m50) / 2.0, 2.0); - - double accuracyValue = 2.83 * Math.Pow(1.52163, (79.5 - 2 * Math.Sqrt(variance)) / 6.0) - * Math.Pow(Math.Log(1.0 + (Math.E - 1.0) * (Math.Min(amountHitObjectsWithAccuracy, 1600) / 1000.0)), 0.5); + // Bonus for many hitcircles - it's harder to keep good accuracy up for longer. + accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3)); if (mods.Any(m => m is OsuModHidden)) accuracyValue *= 1.08; From 86240cc8ecf570e9c9eba1760db1d651d67c2c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 3 Oct 2021 23:36:39 +0200 Subject: [PATCH 170/391] Add alternate Torus font --- osu.Game/Graphics/OsuFont.cs | 6 ++++++ osu.Game/OsuGameBase.cs | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/osu.Game/Graphics/OsuFont.cs b/osu.Game/Graphics/OsuFont.cs index b6090d0e1a..edb484021c 100644 --- a/osu.Game/Graphics/OsuFont.cs +++ b/osu.Game/Graphics/OsuFont.cs @@ -21,6 +21,8 @@ namespace osu.Game.Graphics public static FontUsage Torus => GetFont(Typeface.Torus, weight: FontWeight.Regular); + public static FontUsage TorusAlternate => GetFont(Typeface.TorusAlternate, weight: FontWeight.Regular); + public static FontUsage Inter => GetFont(Typeface.Inter, weight: FontWeight.Regular); /// @@ -57,6 +59,9 @@ namespace osu.Game.Graphics case Typeface.Torus: return "Torus"; + case Typeface.TorusAlternate: + return "Torus-Alternate"; + case Typeface.Inter: return "Inter"; } @@ -113,6 +118,7 @@ namespace osu.Game.Graphics { Venera, Torus, + TorusAlternate, Inter, } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index adb819bf20..02de92e805 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -347,6 +347,11 @@ namespace osu.Game AddFont(Resources, @"Fonts/Torus/Torus-SemiBold"); AddFont(Resources, @"Fonts/Torus/Torus-Bold"); + AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-Regular"); + AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-Light"); + AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-SemiBold"); + AddFont(Resources, @"Fonts/Torus-Alternate/Torus-Alternate-Bold"); + AddFont(Resources, @"Fonts/Inter/Inter-Regular"); AddFont(Resources, @"Fonts/Inter/Inter-RegularItalic"); AddFont(Resources, @"Fonts/Inter/Inter-Light"); From 67d08a3eeee036fc94fca611e4986efa3002c372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Oct 2021 00:20:16 +0200 Subject: [PATCH 171/391] Add test scene for previewing Torus alternates --- .../Visual/UserInterface/TestSceneOsuFont.cs | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneOsuFont.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOsuFont.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuFont.cs new file mode 100644 index 0000000000..eedafce271 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOsuFont.cs @@ -0,0 +1,77 @@ +// 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.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneOsuFont : OsuTestScene + { + private OsuSpriteText spriteText; + + private readonly BindableBool useAlternates = new BindableBool(); + private readonly Bindable weight = new Bindable(FontWeight.Regular); + + [BackgroundDependencyLoader] + private void load() + { + Child = spriteText = new OsuSpriteText + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AllowMultiline = true, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + useAlternates.BindValueChanged(_ => updateFont()); + weight.BindValueChanged(_ => updateFont(), true); + } + + private void updateFont() + { + FontUsage usage = useAlternates.Value ? OsuFont.TorusAlternate : OsuFont.Torus; + spriteText.Font = usage.With(size: 40, weight: weight.Value); + } + + [Test] + public void TestTorusAlternates() + { + AddStep("set all ASCII letters", () => spriteText.Text = @"ABCDEFGHIJKLMNOPQRSTUVWXYZ +abcdefghijklmnopqrstuvwxyz"); + AddStep("set all alternates", () => spriteText.Text = @"A Á Ă Â Ä À Ā Ą Å Ã +Æ B D Ð Ď Đ E É Ě Ê +Ë Ė È Ē Ę F G Ğ Ģ Ġ +H I Í Î Ï İ Ì Ī Į K +Ķ O Œ P Þ Q R Ŕ Ř Ŗ +T Ŧ Ť Ţ Ț V W Ẃ Ŵ Ẅ +Ẁ X Y Ý Ŷ Ÿ Ỳ a á ă +â ä à ā ą å ã æ b d +ď đ e é ě ê ë ė è ē +ę f g ğ ģ ġ k ķ m n +ń ň ņ ŋ ñ o œ p þ q +t ŧ ť ţ ț u ú û ü ù +ű ū ų ů w ẃ ŵ ẅ ẁ x +y ý ŷ ÿ ỳ"); + + AddToggleStep("toggle alternates", alternates => useAlternates.Value = alternates); + + addSetWeightStep(FontWeight.Light); + addSetWeightStep(FontWeight.Regular); + addSetWeightStep(FontWeight.SemiBold); + addSetWeightStep(FontWeight.Bold); + + void addSetWeightStep(FontWeight newWeight) => AddStep($"set weight {newWeight}", () => weight.Value = newWeight); + } + } +} From 017756cbcae754236a4e6cdb3b37f0301121b6be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 4 Oct 2021 00:21:36 +0200 Subject: [PATCH 172/391] Use Torus alternates on online play screens as per design --- osu.Game/Screens/OnlinePlay/Header.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Header.cs b/osu.Game/Screens/OnlinePlay/Header.cs index b0db9256f5..2d4b5cc527 100644 --- a/osu.Game/Screens/OnlinePlay/Header.cs +++ b/osu.Game/Screens/OnlinePlay/Header.cs @@ -72,21 +72,21 @@ namespace osu.Game.Screens.OnlinePlay { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 24), + Font = OsuFont.TorusAlternate.With(size: 24), Text = mainTitle }, dot = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 24), + Font = OsuFont.TorusAlternate.With(size: 24), Text = "·" }, pageTitle = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 24), + Font = OsuFont.TorusAlternate.With(size: 24), Text = "Lounge" } } From 11e9c16b92eec768d23c389329600ca89cf0e2aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 11:13:46 +0900 Subject: [PATCH 173/391] Update resources --- 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 b84f1730ac..eeca40e73d 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,7 +51,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c162025f1f..33d4e5a6c8 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -37,7 +37,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 8597a06c03..e30722c334 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 537b29654e60e738128fa123345a01ab39074b22 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 14:30:22 +0900 Subject: [PATCH 174/391] Fix stream being held open causing windows CI failures --- osu.Game.Tests/Database/RealmTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs index 219690db30..576f901c1a 100644 --- a/osu.Game.Tests/Database/RealmTest.cs +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -70,7 +70,8 @@ namespace osu.Game.Tests.Database { try { - return testStorage.GetStream(realmFactory.Filename)?.Length ?? 0; + using (var stream = testStorage.GetStream(realmFactory.Filename)) + return stream?.Length ?? 0; } catch { From c6aba3e78b3a9f8cf89e60c5f4b069c2873532ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 14:44:16 +0900 Subject: [PATCH 175/391] Ensure a `DrawableChannel` is not attempted to be added after disposal --- osu.Game/Overlays/ChatOverlay.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 25c5154d4a..a61b80cc8e 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -284,6 +284,10 @@ namespace osu.Game.Overlays if (currentChannel.Value != e.NewValue) return; + // check once more to ensure the channel hasn't since been removed from the loaded channels like (may have been left by some automated means). + if (loadedChannels.Contains(loaded)) + return; + loading.Hide(); currentChannelContainer.Clear(false); @@ -426,7 +430,7 @@ namespace osu.Game.Overlays base.PopOut(); } - private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) + private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) => Schedule(() => { switch (args.Action) { @@ -444,10 +448,9 @@ namespace osu.Game.Overlays if (loaded != null) { - loadedChannels.Remove(loaded); - // Because the container is only cleared in the async load callback of a new channel, it is forcefully cleared // to ensure that the previous channel doesn't get updated after it's disposed + loadedChannels.Remove(loaded); currentChannelContainer.Remove(loaded); loaded.Dispose(); } @@ -455,7 +458,7 @@ namespace osu.Game.Overlays break; } - } + }); private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) { From c19c2335eccdd752a521db4b869abf7d96428c19 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 14:58:54 +0900 Subject: [PATCH 176/391] Remove added schedule due to changing flow --- osu.Game/Overlays/ChatOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index a61b80cc8e..7be9258248 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -430,7 +430,7 @@ namespace osu.Game.Overlays base.PopOut(); } - private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) => Schedule(() => + private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) { switch (args.Action) { @@ -458,7 +458,7 @@ namespace osu.Game.Overlays break; } - }); + } private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) { From bc984dff4f2b48d1a4975dfdb031eb378e4d2045 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 15:35:28 +0900 Subject: [PATCH 177/391] Fix typo --- osu.Game/Overlays/ChatOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 7be9258248..20d637d957 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -284,7 +284,7 @@ namespace osu.Game.Overlays if (currentChannel.Value != e.NewValue) return; - // check once more to ensure the channel hasn't since been removed from the loaded channels like (may have been left by some automated means). + // check once more to ensure the channel hasn't since been removed from the loaded channels list (may have been left by some automated means). if (loadedChannels.Contains(loaded)) return; From 5aaafce597e370f9c6c900d3c9f9e992499e0361 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 15:40:00 +0900 Subject: [PATCH 178/391] Make `AuthenticateWithLogin` throw instead of return a `bool` success status --- .../ErrorTextFlowContainer.cs | 2 +- osu.Game/Online/API/OAuth.cs | 37 ++++++++++++++++--- 2 files changed, 32 insertions(+), 7 deletions(-) rename osu.Game/{Overlays/AccountCreation => Graphics}/ErrorTextFlowContainer.cs (95%) diff --git a/osu.Game/Overlays/AccountCreation/ErrorTextFlowContainer.cs b/osu.Game/Graphics/ErrorTextFlowContainer.cs similarity index 95% rename from osu.Game/Overlays/AccountCreation/ErrorTextFlowContainer.cs rename to osu.Game/Graphics/ErrorTextFlowContainer.cs index 87ff4dd398..f17a2a2c3d 100644 --- a/osu.Game/Overlays/AccountCreation/ErrorTextFlowContainer.cs +++ b/osu.Game/Graphics/ErrorTextFlowContainer.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics; using osu.Game.Graphics.Containers; using osuTK.Graphics; -namespace osu.Game.Overlays.AccountCreation +namespace osu.Game.Graphics { public class ErrorTextFlowContainer : OsuTextFlowContainer { diff --git a/osu.Game/Online/API/OAuth.cs b/osu.Game/Online/API/OAuth.cs index bdc47aab8d..693e7c5336 100644 --- a/osu.Game/Online/API/OAuth.cs +++ b/osu.Game/Online/API/OAuth.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.Diagnostics; using System.Net.Http; +using Newtonsoft.Json; using osu.Framework.Bindables; namespace osu.Game.Online.API @@ -32,10 +34,10 @@ namespace osu.Game.Online.API this.endpoint = endpoint; } - internal bool AuthenticateWithLogin(string username, string password) + internal void AuthenticateWithLogin(string username, string password) { - if (string.IsNullOrEmpty(username)) return false; - if (string.IsNullOrEmpty(password)) return false; + if (string.IsNullOrEmpty(username)) throw new ArgumentException("Missing username."); + if (string.IsNullOrEmpty(password)) throw new ArgumentException("Missing password."); using (var req = new AccessTokenRequestPassword(username, password) { @@ -49,13 +51,27 @@ namespace osu.Game.Online.API { req.Perform(); } - catch + catch (Exception ex) { - return false; + Token.Value = null; + + var throwableException = ex; + + try + { + // attempt to decode a displayable error string. + var error = JsonConvert.DeserializeObject(req.GetResponseString() ?? string.Empty); + if (error != null) + throwableException = new APIException(error.Message, ex); + } + catch + { + } + + throw throwableException; } Token.Value = req.ResponseObject; - return true; } } @@ -182,5 +198,14 @@ namespace osu.Game.Online.API base.PrePerform(); } } + + private class OAuthError + { + [JsonProperty("error")] + public string ErrorType { get; set; } + + [JsonProperty("hint")] + public string Message { get; set; } + } } } From 266b4c7124fe4b578087afaaba46e18ec4ee0423 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 15:40:24 +0900 Subject: [PATCH 179/391] Expose login errors from `IAPIProvider` and show on the login form --- .../Visual/Menus/TestSceneLoginPanel.cs | 16 ++++++++++- osu.Game/Online/API/APIAccess.cs | 28 ++++++++++++------- osu.Game/Online/API/DummyAPIAccess.cs | 18 ++++++++++++ osu.Game/Online/API/IAPIProvider.cs | 6 ++++ osu.Game/Overlays/Login/LoginForm.cs | 14 +++++++++- 5 files changed, 70 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs b/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs index 5fdadfc2fb..4754a73f83 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneLoginPanel.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Overlays.Login; namespace osu.Game.Tests.Visual.Menus @@ -30,12 +31,25 @@ namespace osu.Game.Tests.Visual.Menus } [Test] - public void TestBasicLogin() + public void TestLoginSuccess() { AddStep("logout", () => API.Logout()); AddStep("enter password", () => loginPanel.ChildrenOfType().First().Text = "password"); AddStep("submit", () => loginPanel.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); } + + [Test] + public void TestLoginFailure() + { + AddStep("logout", () => + { + API.Logout(); + ((DummyAPIAccess)API).FailNextLogin(); + }); + + AddStep("enter password", () => loginPanel.ChildrenOfType().First().Text = "password"); + AddStep("submit", () => loginPanel.ChildrenOfType().First(b => b.Text.ToString() == "Sign in").TriggerClick()); + } } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index af14cdc7b3..94508e3a81 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -35,9 +35,8 @@ namespace osu.Game.Online.API public string WebsiteRootUrl { get; } - /// - /// The username/email provided by the user when initiating a login. - /// + public Exception LastLoginError { get; private set; } + public string ProvidedUsername { get; private set; } private string password; @@ -136,14 +135,23 @@ namespace osu.Game.Online.API // save the username at this point, if the user requested for it to be. config.SetValue(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); - if (!authentication.HasValidAccessToken && !authentication.AuthenticateWithLogin(ProvidedUsername, password)) + if (!authentication.HasValidAccessToken) { - //todo: this fails even on network-related issues. we should probably handle those differently. - //NotificationOverlay.ShowMessage("Login failed!"); - log.Add(@"Login failed!"); - password = null; - authentication.Clear(); - continue; + LastLoginError = null; + + try + { + authentication.AuthenticateWithLogin(ProvidedUsername, password); + } + catch (Exception e) + { + //todo: this fails even on network-related issues. we should probably handle those differently. + LastLoginError = e; + log.Add(@"Login failed!"); + password = null; + authentication.Clear(); + continue; + } } var userReq = new GetUserRequest(); diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 1ba31db9fa..8f91a4d198 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -32,6 +32,8 @@ namespace osu.Game.Online.API public string WebsiteRootUrl => "http://localhost"; + public Exception LastLoginError { get; private set; } + /// /// Provide handling logic for an arbitrary API request. /// Should return true is a request was handled. If null or false return, the request will be failed with a . @@ -40,6 +42,8 @@ namespace osu.Game.Online.API private readonly Bindable state = new Bindable(APIState.Online); + private bool shouldFailNextLogin; + /// /// The current connectivity state of the API. /// @@ -74,6 +78,18 @@ namespace osu.Game.Online.API public void Login(string username, string password) { + state.Value = APIState.Connecting; + + if (shouldFailNextLogin) + { + LastLoginError = new APIException("Not powerful enough to login.", new ArgumentException(nameof(shouldFailNextLogin))); + + state.Value = APIState.Offline; + shouldFailNextLogin = false; + return; + } + + LastLoginError = null; LocalUser.Value = new User { Username = username, @@ -102,5 +118,7 @@ namespace osu.Game.Online.API IBindable IAPIProvider.LocalUser => LocalUser; IBindableList IAPIProvider.Friends => Friends; IBindable IAPIProvider.Activity => Activity; + + public void FailNextLogin() => shouldFailNextLogin = true; } } diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 5ad5367924..72ca37bcf4 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -3,6 +3,7 @@ #nullable enable +using System; using System.Threading.Tasks; using osu.Framework.Bindables; using osu.Game.Users; @@ -55,6 +56,11 @@ namespace osu.Game.Online.API /// string WebsiteRootUrl { get; } + /// + /// The last login error that occurred, if any. + /// + Exception? LastLoginError { get; } + /// /// The current connection state of the API. /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. diff --git a/osu.Game/Overlays/Login/LoginForm.cs b/osu.Game/Overlays/Login/LoginForm.cs index e43b84d52a..f7842dcd30 100644 --- a/osu.Game/Overlays/Login/LoginForm.cs +++ b/osu.Game/Overlays/Login/LoginForm.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; @@ -42,6 +43,9 @@ namespace osu.Game.Overlays.Login Spacing = new Vector2(0, 5); AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; + + ErrorTextFlowContainer errorText; + Children = new Drawable[] { username = new OsuTextBox @@ -57,6 +61,11 @@ namespace osu.Game.Overlays.Login RelativeSizeAxes = Axes.X, TabbableContentContainer = this, }, + errorText = new ErrorTextFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, new SettingsCheckbox { LabelText = "Remember username", @@ -97,6 +106,9 @@ namespace osu.Game.Overlays.Login }; password.OnCommit += (sender, newText) => performLogin(); + + if (api?.LastLoginError?.Message is string error) + errorText.AddErrors(new[] { error }); } public override bool AcceptsFocus => true; @@ -108,4 +120,4 @@ namespace osu.Game.Overlays.Login Schedule(() => { GetContainingInputManager().ChangeFocus(string.IsNullOrEmpty(username.Text) ? username : password); }); } } -} \ No newline at end of file +} From 4e1322effac966e1b3fec6dab2231d131648defc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 16:02:45 +0900 Subject: [PATCH 180/391] Fix typo --- osu.Game/Beatmaps/BeatmapModelManager.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index aa14f95863..250d6653d5 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -185,12 +185,12 @@ namespace osu.Game.Beatmaps /// /// Saves an file against a given . /// - /// The to save the content against. The file referenced by will be replaced. + /// The to save the content against. The file referenced by will be replaced. /// The content to write. /// The beatmap content to write, null if to be omitted. - public virtual void Save(BeatmapInfo baetmapInfo, IBeatmap beatmapContent, ISkin beatmapSkin = null) + public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin beatmapSkin = null) { - var setInfo = baetmapInfo.BeatmapSet; + var setInfo = beatmapInfo.BeatmapSet; using (var stream = new MemoryStream()) { @@ -201,7 +201,7 @@ namespace osu.Game.Beatmaps using (ContextFactory.GetForWrite()) { - var beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == baetmapInfo.ID); + beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == beatmapInfo.ID); var metadata = beatmapInfo.Metadata ?? setInfo.Metadata; // grab the original file (or create a new one if not found). @@ -219,7 +219,7 @@ namespace osu.Game.Beatmaps } } - WorkingBeatmapCache?.Invalidate(baetmapInfo); + WorkingBeatmapCache?.Invalidate(beatmapInfo); } /// From 3a0b7ba8fffe61e85b8adceebf48a7eeffb10fd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 16:18:55 +0900 Subject: [PATCH 181/391] Add fallback to use `Message` when `Hint` is not available --- osu.Game/Online/API/OAuth.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/OAuth.cs b/osu.Game/Online/API/OAuth.cs index 693e7c5336..d79fc58d1c 100644 --- a/osu.Game/Online/API/OAuth.cs +++ b/osu.Game/Online/API/OAuth.cs @@ -62,7 +62,7 @@ namespace osu.Game.Online.API // attempt to decode a displayable error string. var error = JsonConvert.DeserializeObject(req.GetResponseString() ?? string.Empty); if (error != null) - throwableException = new APIException(error.Message, ex); + throwableException = new APIException(error.UserDisplayableError, ex); } catch { @@ -201,10 +201,15 @@ namespace osu.Game.Online.API private class OAuthError { + public string UserDisplayableError => !string.IsNullOrEmpty(Hint) ? Hint : ErrorIdentifier; + [JsonProperty("error")] - public string ErrorType { get; set; } + public string ErrorIdentifier { get; set; } [JsonProperty("hint")] + public string Hint { get; set; } + + [JsonProperty("message")] public string Message { get; set; } } } From 3c15ef720f96e0cfd52cd4f7b90929a834ff2f6f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 16:26:28 +0900 Subject: [PATCH 182/391] Remove setter from `IHasGuidPrimaryKey` interface --- osu.Game/Database/IHasGuidPrimaryKey.cs | 2 +- osu.Game/Database/ILive.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/IHasGuidPrimaryKey.cs b/osu.Game/Database/IHasGuidPrimaryKey.cs index c9cd9b257a..f52dc5c8ef 100644 --- a/osu.Game/Database/IHasGuidPrimaryKey.cs +++ b/osu.Game/Database/IHasGuidPrimaryKey.cs @@ -11,6 +11,6 @@ namespace osu.Game.Database { [JsonIgnore] [PrimaryKey] - Guid ID { get; set; } + Guid ID { get; } } } diff --git a/osu.Game/Database/ILive.cs b/osu.Game/Database/ILive.cs index 29e5756dba..9359b09eaf 100644 --- a/osu.Game/Database/ILive.cs +++ b/osu.Game/Database/ILive.cs @@ -9,7 +9,7 @@ namespace osu.Game.Database /// A wrapper to provide access to database backed classes in a thread-safe manner. /// /// The databased type. - public interface ILive where T : class + public interface ILive where T : class // TODO: Add IHasGuidPrimaryKey once we don't need EF support any more. { Guid ID { get; } From 857000b756765a99337d4b22c5085d4a27fdabfd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 16:29:46 +0900 Subject: [PATCH 183/391] Mark `IPresentImports` as covariant --- osu.Game/Database/IPresentImports.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/IPresentImports.cs b/osu.Game/Database/IPresentImports.cs index 6aa29a5083..fb3aad7ee1 100644 --- a/osu.Game/Database/IPresentImports.cs +++ b/osu.Game/Database/IPresentImports.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; namespace osu.Game.Database { - public interface IPresentImports + public interface IPresentImports where TModel : class { /// From e631653f4b1b471ec3794e7845b34492da88e801 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 16:30:12 +0900 Subject: [PATCH 184/391] Remove incorrectly committed `FodyWeavers` file --- osu.Game/FodyWeavers.xml | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 osu.Game/FodyWeavers.xml diff --git a/osu.Game/FodyWeavers.xml b/osu.Game/FodyWeavers.xml deleted file mode 100644 index cc07b89533..0000000000 --- a/osu.Game/FodyWeavers.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file From 63f0b0c93215cf1b0266b4891e6e82a160641951 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 16:35:55 +0900 Subject: [PATCH 185/391] Rename out of place interface name --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Database/ArchiveModelManager.cs | 8 ++++---- osu.Game/Database/{IPresentImports.cs => IPostImports.cs} | 4 ++-- osu.Game/OsuGame.cs | 2 +- osu.Game/Scoring/ScoreManager.cs | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) rename osu.Game/Database/{IPresentImports.cs => IPostImports.cs} (76%) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index b302df1516..f8181cd010 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -178,7 +178,7 @@ namespace osu.Game.Beatmaps /// /// Fired when the user requests to view the resulting import. /// - public Action>> PresentImport { set => beatmapModelManager.PresentImport = value; } + public Action>> PresentImport { set => beatmapModelManager.PostImport = value; } /// /// Delete a beatmap difficulty. diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 403bfdf621..9ad2dec12e 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -30,7 +30,7 @@ namespace osu.Game.Database /// /// The model type. /// The associated file join type. - public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager, IPresentImports + public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager, IPostImports where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete where TFileModel : class, INamedFileInfo, new() { @@ -200,12 +200,12 @@ namespace osu.Game.Database ? $"Imported {imported.First()}!" : $"Imported {imported.Count} {HumanisedModelName}s!"; - if (imported.Count > 0 && PresentImport != null) + if (imported.Count > 0 && PostImport != null) { notification.CompletionText += " Click to view."; notification.CompletionClickAction = () => { - PresentImport?.Invoke(imported); + PostImport?.Invoke(imported); return true; }; } @@ -249,7 +249,7 @@ namespace osu.Game.Database return import; } - public Action>> PresentImport { protected get; set; } + public Action>> PostImport { protected get; set; } /// /// Silently import an item from an . diff --git a/osu.Game/Database/IPresentImports.cs b/osu.Game/Database/IPostImports.cs similarity index 76% rename from osu.Game/Database/IPresentImports.cs rename to osu.Game/Database/IPostImports.cs index fb3aad7ee1..f09285089a 100644 --- a/osu.Game/Database/IPresentImports.cs +++ b/osu.Game/Database/IPostImports.cs @@ -6,12 +6,12 @@ using System.Collections.Generic; namespace osu.Game.Database { - public interface IPresentImports + public interface IPostImports where TModel : class { /// /// Fired when the user requests to view the resulting import. /// - public Action>> PresentImport { set; } + public Action>> PostImport { set; } } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 35ec213755..64c77c370e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -627,7 +627,7 @@ namespace osu.Game BeatmapManager.PresentImport = items => PresentBeatmap(items.First().Value); ScoreManager.PostNotification = n => Notifications.Post(n); - ScoreManager.PresentImport = items => PresentScore(items.First().Value); + ScoreManager.PostImport = items => PresentScore(items.First().Value); // make config aware of how to lookup skins for on-screen display purposes. // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index aa0ee4bbbb..922b4f0a38 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -25,7 +25,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Scoring { - public class ScoreManager : IModelManager, IModelFileManager, IModelDownloader, ICanAcceptFiles, IPresentImports + public class ScoreManager : IModelManager, IModelFileManager, IModelDownloader, ICanAcceptFiles, IPostImports { private readonly Scheduler scheduler; private readonly Func difficulties; @@ -365,9 +365,9 @@ namespace osu.Game.Scoring #region Implementation of IPresentImports - public Action>> PresentImport + public Action>> PostImport { - set => scoreModelManager.PresentImport = value; + set => scoreModelManager.PostImport = value; } #endregion From 8bfdfe3672997fcacfed8a571629c17403348d9e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 16:54:00 +0900 Subject: [PATCH 186/391] Add literal string marker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Beatmaps/IBeatmapSetInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/IBeatmapSetInfo.cs b/osu.Game/Beatmaps/IBeatmapSetInfo.cs index 548a48367c..d5aded7b53 100644 --- a/osu.Game/Beatmaps/IBeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapSetInfo.cs @@ -53,6 +53,6 @@ namespace osu.Game.Beatmaps /// /// The filename for the storyboard. /// - string StoryboardFile => Files.FirstOrDefault(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename ?? string.Empty; + string StoryboardFile => Files.FirstOrDefault(f => f.Filename.EndsWith(@".osb", StringComparison.OrdinalIgnoreCase))?.Filename ?? string.Empty; } } From 4df5f931522f63553e4d2049b3147f5c7fbb650b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 16:50:29 +0900 Subject: [PATCH 187/391] Inline single usage of `StoryboardFile` to avoid interface default method woes --- osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs | 6 ++++-- osu.Game/Beatmaps/BeatmapSetInfo.cs | 4 ---- osu.Game/Beatmaps/IBeatmapSetInfo.cs | 6 ------ 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 45112ae74c..f9889474ac 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -107,12 +107,14 @@ namespace osu.Game.Beatmaps { var decoder = Decoder.GetDecoder(stream); + var storyboardFilename = BeatmapSetInfo?.Files.FirstOrDefault(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename; + // todo: support loading from both set-wide storyboard *and* beatmap specific. - if (BeatmapSetInfo?.StoryboardFile == null) + if (string.IsNullOrEmpty(storyboardFilename)) storyboard = decoder.Decode(stream); else { - using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapSetInfo.StoryboardFile)))) + using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(storyboardFilename)))) storyboard = decoder.Decode(stream, secondaryStream); } } diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 9f5a07ec43..8b01831b3c 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using JetBrains.Annotations; -using Newtonsoft.Json; using osu.Framework.Testing; using osu.Game.Database; @@ -62,9 +61,6 @@ namespace osu.Game.Beatmaps public string Hash { get; set; } - [JsonIgnore] - public string StoryboardFile => ((IBeatmapSetInfo)this).StoryboardFile; - /// /// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null. /// The path returned is relative to the user file storage. diff --git a/osu.Game/Beatmaps/IBeatmapSetInfo.cs b/osu.Game/Beatmaps/IBeatmapSetInfo.cs index d5aded7b53..0cfb0c4242 100644 --- a/osu.Game/Beatmaps/IBeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapSetInfo.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using osu.Game.Database; #nullable enable @@ -49,10 +48,5 @@ namespace osu.Game.Beatmaps /// The maximum BPM of all beatmaps in this set. /// double MaxBPM { get; } - - /// - /// The filename for the storyboard. - /// - string StoryboardFile => Files.FirstOrDefault(f => f.Filename.EndsWith(@".osb", StringComparison.OrdinalIgnoreCase))?.Filename ?? string.Empty; } } From fd6b10656c638753a15efcb004f8848e109852bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 16:55:16 +0900 Subject: [PATCH 188/391] Add TODO reminder about ruleset reference transfer quirk --- osu.Game/Rulesets/IRulesetInfo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs index d4dec0de64..ded3ac4b58 100644 --- a/osu.Game/Rulesets/IRulesetInfo.cs +++ b/osu.Game/Rulesets/IRulesetInfo.cs @@ -38,6 +38,7 @@ namespace osu.Game.Rulesets var ruleset = Activator.CreateInstance(type) as Ruleset; // overwrite the pre-populated RulesetInfo with a potentially database attached copy. + // TODO: figure if we still want/need this after switching to realm. // ruleset.RulesetInfo = this; return ruleset; From 51b7dce16f2bfb031a135183cdabecbbf2a2359b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 16:55:51 +0900 Subject: [PATCH 189/391] Remove reference to `osu-web-10` --- osu.Game/Beatmaps/IBeatmapInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs index 6153a0af08..d552a3f1e3 100644 --- a/osu.Game/Beatmaps/IBeatmapInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapInfo.cs @@ -50,7 +50,7 @@ namespace osu.Game.Beatmaps string Hash { get; } /// - /// MD5 is kept for legacy support (matching against replays, osu-web-10 etc.). + /// MD5 is kept for legacy support (matching against replays etc.). /// string MD5Hash { get; } From f293e008d953d334ebb291481502679519d2c88c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 17:00:22 +0900 Subject: [PATCH 190/391] Move `BeatmapInfo`'s `SearchableTerms` implementation to interface --- osu.Game/Beatmaps/BeatmapInfo.cs | 5 +---- osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs | 1 + osu.Game/Beatmaps/IBeatmapInfo.cs | 13 +++++++++++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 09f237a5de..cd5f8fb9a1 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -152,10 +152,7 @@ namespace osu.Game.Beatmaps [JsonIgnore] public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarDifficulty); - public string[] SearchableTerms => new[] - { - Version - }.Concat(Metadata?.SearchableTerms ?? Enumerable.Empty()).Where(s => !string.IsNullOrEmpty(s)).ToArray(); + public IEnumerable SearchableTerms => ((IBeatmapInfo)this).SearchableTerms; public override string ToString() => ((IBeatmapInfo)this).DisplayTitle; diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index f9889474ac..5fec8f7a89 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Framework.Logging; diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs index d552a3f1e3..fb30b0279c 100644 --- a/osu.Game/Beatmaps/IBeatmapInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapInfo.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.Localisation; using osu.Game.Database; using osu.Game.Rulesets; @@ -22,7 +23,7 @@ namespace osu.Game.Beatmaps /// /// The metadata representing this beatmap. May be shared between multiple beatmaps. /// - IBeatmapMetadataInfo Metadata { get; } + IBeatmapMetadataInfo? Metadata { get; } /// /// The difficulty settings for this beatmap. @@ -76,12 +77,20 @@ namespace osu.Game.Beatmaps { get { - var metadata = Metadata.DisplayTitleRomanisable; + var metadata = closestMetadata.DisplayTitleRomanisable; return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim()); } } + string[] SearchableTerms => new[] + { + DifficultyName + }.Concat(closestMetadata.SearchableTerms).Where(s => !string.IsNullOrEmpty(s)).ToArray(); + private string versionString => string.IsNullOrEmpty(DifficultyName) ? string.Empty : $"[{DifficultyName}]"; + + // temporary helper methods until we figure which metadata should be where. + private IBeatmapMetadataInfo closestMetadata => (Metadata ?? BeatmapSet.Metadata)!; } } From 95f1cc85d4b57a5eb710da9ace0cc00d87952197 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 17:12:40 +0900 Subject: [PATCH 191/391] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 8fad10d247..fb3e4b3bbd 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ba118c5240..e53b83100e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 37931d0c38..4e7053d816 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From 6ac0601d2cdba14f61f3bc589ab8198f5ecb63da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 17:18:08 +0900 Subject: [PATCH 192/391] Fix incorrect csproj merge --- 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 36c5fd89bf..4877ddf725 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 853cf6feaa165e833ecb7ca18c6cdffe8ca6e005 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Oct 2021 17:35:53 +0900 Subject: [PATCH 193/391] Rename last remaining `BeatmapInfo Beatmap` usage --- .../TestSceneAccuracyHeatmap.cs | 2 +- .../Beatmaps/IO/ImportBeatmapTest.cs | 2 +- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 4 ++-- .../Background/TestSceneUserDimBackgrounds.cs | 2 +- .../Gameplay/TestSceneReplayRecorder.cs | 2 +- .../Gameplay/TestSceneReplayRecording.cs | 2 +- .../Gameplay/TestSceneSpectatorPlayback.cs | 2 +- .../TestSceneMultiplayerResults.cs | 2 +- .../TestSceneMultiplayerTeamResults.cs | 2 +- .../Navigation/TestScenePresentScore.cs | 2 +- .../Online/TestSceneUserProfileScores.cs | 8 ++++---- .../Visual/Ranking/TestSceneAccuracyCircle.cs | 2 +- .../TestSceneExpandedPanelMiddleContent.cs | 8 ++++---- .../Visual/Ranking/TestSceneResultsScreen.cs | 4 ++-- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 20 +++++++++---------- .../SongSelect/TestScenePlaySongSelect.cs | 4 ++-- .../TestSceneDeleteLocalScore.cs | 2 +- .../Requests/Responses/APILegacyScoreInfo.cs | 2 +- osu.Game/Online/Rooms/MultiplayerScore.cs | 2 +- osu.Game/Online/Spectator/SpectatorClient.cs | 2 +- osu.Game/OsuGame.cs | 2 +- osu.Game/OsuGameBase.cs | 2 +- .../Overlays/BeatmapSet/Scores/ScoreTable.cs | 2 +- .../BeatmapSet/Scores/ScoresContainer.cs | 2 +- .../Scores/TopScoreStatisticsSection.cs | 2 +- .../Sections/Ranks/DrawableProfileScore.cs | 4 ++-- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 4 ++-- osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs | 4 ++-- osu.Game/Scoring/ScoreInfo.cs | 5 +++-- osu.Game/Scoring/ScoreManager.cs | 12 +++++------ osu.Game/Scoring/ScorePerformanceCache.cs | 2 +- osu.Game/Scoring/ScoreStore.cs | 6 +++--- .../Multiplayer/Spectate/PlayerArea.cs | 2 +- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Screens/Play/SoloPlayer.cs | 2 +- .../Expanded/ExpandedPanelMiddleContent.cs | 2 +- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 4 ++-- .../Ranking/Statistics/StatisticsPanel.cs | 2 +- .../Select/BeatmapClearScoresDialog.cs | 2 +- .../Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- osu.Game/Screens/Select/PlaySongSelect.cs | 2 +- osu.Game/Screens/Spectate/SpectatorScreen.cs | 2 +- osu.Game/Tests/TestScoreInfo.cs | 2 +- 43 files changed, 74 insertions(+), 73 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs index 10d9d7ffde..79150a1941 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneAccuracyHeatmap.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Tests { Position = new Vector2(100, 300), }, - accuracyHeatmap = new TestAccuracyHeatmap(new ScoreInfo { Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }) + accuracyHeatmap = new TestAccuracyHeatmap(new ScoreInfo { BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index b536fc61b7..dce01448f4 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -950,7 +950,7 @@ namespace osu.Game.Tests.Beatmaps.IO return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, BeatmapInfoID = beatmapInfo.ID }, new ImportScoreTest.TestArchiveReader()); } diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index cd7d744f53..2cd02329b7 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -141,7 +141,7 @@ namespace osu.Game.Tests.Scores.IO var beatmapManager = osu.Dependencies.Get(); var scoreManager = osu.Dependencies.Get(); - beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.Beatmap.ID))); + beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.BeatmapInfo.ID))); Assert.That(scoreManager.Query(s => s.ID == imported.ID).DeletePending, Is.EqualTo(true)); var secondImport = await LoadScoreIntoOsu(osu, imported); @@ -181,7 +181,7 @@ namespace osu.Game.Tests.Scores.IO { var beatmapManager = osu.Dependencies.Get(); - score.Beatmap ??= beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); + score.BeatmapInfo ??= beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(); score.Ruleset ??= new OsuRuleset().RulesetInfo; var scoreManager = osu.Dependencies.Get(); diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 12a85c3f26..693c66ccb0 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -232,7 +232,7 @@ namespace osu.Game.Tests.Visual.Background AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo { User = new User { Username = "osu!" }, - Beatmap = new TestBeatmap(Ruleset.Value).BeatmapInfo, + BeatmapInfo = new TestBeatmap(Ruleset.Value).BeatmapInfo, Ruleset = Ruleset.Value, }))); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index d89fd322d1..c8040f42f0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Gameplay Recorder = recorder = new TestReplayRecorder(new Score { Replay = replay, - ScoreInfo = { Beatmap = gameplayState.Beatmap.BeatmapInfo } + ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo } }) { ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs index 07514ad51a..3545fc96e8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Gameplay Recorder = new TestReplayRecorder(new Score { Replay = replay, - ScoreInfo = { Beatmap = gameplayState.Beatmap.BeatmapInfo } + ScoreInfo = { BeatmapInfo = gameplayState.Beatmap.BeatmapInfo } }) { ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 07ff35f77b..b4de060578 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -356,7 +356,7 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestReplayRecorder : ReplayRecorder { public TestReplayRecorder() - : base(new Score { ScoreInfo = { Beatmap = new BeatmapInfo() } }) + : base(new Score { ScoreInfo = { BeatmapInfo = new BeatmapInfo() } }) { } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs index ff06d4d9c7..5032cdaec7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerResults.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Accuracy = 0.8, MaxCombo = 500, Combo = 250, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Username = "Test user" }, Date = DateTimeOffset.Now, OnlineScoreID = 12345, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs index 0a8bda7ec0..99d5fd46e9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerTeamResults.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Accuracy = 0.8, MaxCombo = 500, Combo = 250, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Username = "Test user" }, Date = DateTimeOffset.Now, OnlineScoreID = 12345, diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index 52b577b402..ee84d775d2 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Navigation { Hash = Guid.NewGuid().ToString(), OnlineScoreID = i, - Beatmap = beatmap.Beatmaps.First(), + BeatmapInfo = beatmap.Beatmaps.First(), Ruleset = ruleset ?? new OsuRuleset().RulesetInfo }).Result; }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs index 5dca218531..513631a221 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileScores.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Online { PP = 1047.21, Rank = ScoreRank.SH, - Beatmap = new BeatmapInfo + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Online { PP = 134.32, Rank = ScoreRank.A, - Beatmap = new BeatmapInfo + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { @@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.Online { PP = 96.83, Rank = ScoreRank.S, - Beatmap = new BeatmapInfo + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { @@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.Online var noPPScore = new ScoreInfo { Rank = ScoreRank.B, - Beatmap = new BeatmapInfo + BeatmapInfo = new BeatmapInfo { Metadata = new BeatmapMetadata { diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index a5e2f02f31..df8500fab2 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -71,7 +71,7 @@ namespace osu.Game.Tests.Visual.Ranking Id = 2, Username = "peppy", }, - Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, + BeatmapInfo = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, TotalScore = 2845370, Accuracy = accuracy, diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 5180854aba..899f351a2a 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("show example score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo) { - Beatmap = createTestBeatmap(author) + BeatmapInfo = createTestBeatmap(author) })); } @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("show excess mods score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo, true) { - Beatmap = createTestBeatmap(author) + BeatmapInfo = createTestBeatmap(author) })); AddAssert("mapper name present", () => this.ChildrenOfType().Any(spriteText => spriteText.Current.Value == "mapper_name")); @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Ranking { AddStep("show example score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo) { - Beatmap = createTestBeatmap(null) + BeatmapInfo = createTestBeatmap(null) })); AddAssert("mapped by text not present", () => @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Ranking showPanel(new TestScoreInfo(ruleset.RulesetInfo) { Mods = mods, - Beatmap = beatmap, + BeatmapInfo = beatmap, Date = default, }); }); diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 631455b727..8d5d0ba8c7 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -337,8 +337,8 @@ namespace osu.Game.Tests.Visual.Ranking public UnrankedSoloResultsScreen(ScoreInfo score) : base(score, true) { - Score.Beatmap.OnlineBeatmapID = 0; - Score.Beatmap.Status = BeatmapSetOnlineStatus.Pending; + Score.BeatmapInfo.OnlineBeatmapID = 0; + Score.BeatmapInfo.Status = BeatmapSetOnlineStatus.Pending; } protected override void LoadComplete() diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 95cf6a9903..13b769c80a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -197,7 +197,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 6602580, @@ -216,7 +216,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 4608074, @@ -235,7 +235,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 1014222, @@ -254,7 +254,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 1541390, @@ -273,7 +273,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 2243452, @@ -292,7 +292,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 2705430, @@ -311,7 +311,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 7151382, @@ -330,7 +330,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 2051389, @@ -349,7 +349,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 6169483, @@ -368,7 +368,7 @@ namespace osu.Game.Tests.Visual.SongSelect MaxCombo = 244, TotalScore = 1707827, //Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, User = new User { Id = 6702666, diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index f9e81d3da6..78040b3d6a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -805,7 +805,7 @@ namespace osu.Game.Tests.Visual.SongSelect songSelect.PresentScore(new ScoreInfo { User = new User { Username = "woo" }, - Beatmap = getPresentBeatmap(), + BeatmapInfo = getPresentBeatmap(), Ruleset = getPresentBeatmap().Ruleset }); }); @@ -837,7 +837,7 @@ namespace osu.Game.Tests.Visual.SongSelect songSelect.PresentScore(new ScoreInfo { User = new User { Username = "woo" }, - Beatmap = getPresentBeatmap(), + BeatmapInfo = getPresentBeatmap(), Ruleset = getPresentBeatmap().Ruleset }); }); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index f58dbef145..c237fcaebf 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.UserInterface var score = new ScoreInfo { OnlineScoreID = i, - Beatmap = beatmapInfo, + BeatmapInfo = beatmapInfo, BeatmapInfoID = beatmapInfo.ID, Accuracy = RNG.NextDouble(), TotalScore = RNG.Next(1, 1000000), diff --git a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs index 18a0db3928..aaf2dccc82 100644 --- a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs @@ -37,7 +37,7 @@ namespace osu.Game.Online.API.Requests.Responses OnlineScoreID = OnlineScoreID, Date = Date, PP = PP, - Beatmap = BeatmapInfo, + BeatmapInfo = BeatmapInfo, RulesetID = OnlineRulesetID, Hash = Replay ? "online" : string.Empty, // todo: temporary? Rank = Rank, diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index 30c1d2f826..7ec34e70d5 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -70,7 +70,7 @@ namespace osu.Game.Online.Rooms OnlineScoreID = ID, TotalScore = TotalScore, MaxCombo = MaxCombo, - Beatmap = playlistItem.Beatmap.Value, + BeatmapInfo = playlistItem.Beatmap.Value, BeatmapInfoID = playlistItem.BeatmapID, Ruleset = playlistItem.Ruleset.Value, RulesetID = playlistItem.RulesetID, diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index d55ad45ff5..b597b2f214 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -144,7 +144,7 @@ namespace osu.Game.Online.Spectator IsPlaying = true; // transfer state at point of beginning play - currentState.BeatmapID = score.ScoreInfo.Beatmap.OnlineBeatmapID; + currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineBeatmapID; currentState.RulesetID = score.ScoreInfo.RulesetID; currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray(); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 99925bb1fb..9dd879fd7e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -482,7 +482,7 @@ namespace osu.Game return; } - var databasedBeatmap = BeatmapManager.QueryBeatmap(b => b.ID == databasedScoreInfo.Beatmap.ID); + var databasedBeatmap = BeatmapManager.QueryBeatmap(b => b.ID == databasedScoreInfo.BeatmapInfo.ID); if (databasedBeatmap == null) { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 02de92e805..aec06e18f6 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -250,7 +250,7 @@ namespace osu.Game List getBeatmapScores(BeatmapSetInfo set) { var beatmapIds = BeatmapManager.QueryBeatmaps(b => b.BeatmapSetInfoID == set.ID).Select(b => b.ID).ToList(); - return ScoreManager.QueryScores(s => beatmapIds.Contains(s.Beatmap.ID)).ToList(); + return ScoreManager.QueryScores(s => beatmapIds.Contains(s.BeatmapInfo.ID)).ToList(); } BeatmapManager.ItemRemoved.BindValueChanged(i => diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 8fe1d35b62..018faf2011 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -172,7 +172,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { Text = score.MaxCombo.ToLocalisableString(@"0\x"), Font = OsuFont.GetFont(size: text_size), - Colour = score.MaxCombo == score.Beatmap?.MaxCombo ? highAccuracyColour : Color4.White + Colour = score.MaxCombo == score.BeatmapInfo?.MaxCombo ? highAccuracyColour : Color4.White } }; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index fb1769fbe1..82657afc86 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -74,7 +74,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores var topScore = ordered.Result.First(); - scoreTable.DisplayScores(ordered.Result, topScore.Beatmap?.Status.GrantsPerformancePoints() == true); + scoreTable.DisplayScores(ordered.Result, topScore.BeatmapInfo?.Status.GrantsPerformancePoints() == true); scoreTable.Show(); var userScore = value.UserScore; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 883e83ce6e..630aa8fe53 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -115,7 +115,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores accuracyColumn.Text = value.DisplayAccuracy; maxComboColumn.Text = value.MaxCombo.ToLocalisableString(@"0\x"); - ppColumn.Alpha = value.Beatmap?.Status.GrantsPerformancePoints() == true ? 1 : 0; + ppColumn.Alpha = value.BeatmapInfo?.Status.GrantsPerformancePoints() == true ? 1 : 0; ppColumn.Text = value.PP?.ToLocalisableString(@"N0"); statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(createStatisticsColumn); diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs index c221f070df..3561e9700e 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs @@ -78,7 +78,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks Spacing = new Vector2(0, 2), Children = new Drawable[] { - new ScoreBeatmapMetadataContainer(Score.Beatmap), + new ScoreBeatmapMetadataContainer(Score.BeatmapInfo), new FillFlowContainer { AutoSizeAxes = Axes.Both, @@ -88,7 +88,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks { new OsuSpriteText { - Text = $"{Score.Beatmap.Version}", + Text = $"{Score.BeatmapInfo.Version}", Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), Colour = colours.Yellow }, diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 2e1a29372d..a1658b4cf3 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -70,7 +70,7 @@ namespace osu.Game.Scoring.Legacy scoreInfo.Mods = scoreInfo.Mods.Append(currentRuleset.CreateMod()).ToArray(); currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods); - scoreInfo.Beatmap = currentBeatmap.BeatmapInfo; + scoreInfo.BeatmapInfo = currentBeatmap.BeatmapInfo; /* score.HpGraphString = */ sr.ReadString(); @@ -119,7 +119,7 @@ namespace osu.Game.Scoring.Legacy // 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.Beatmap = workingBeatmap.BeatmapInfo; + score.ScoreInfo.BeatmapInfo = workingBeatmap.BeatmapInfo; return score; } diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index 288552879c..58e4192f77 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -34,7 +34,7 @@ namespace osu.Game.Scoring.Legacy this.score = score; this.beatmap = beatmap; - if (score.ScoreInfo.Beatmap.RulesetID < 0 || score.ScoreInfo.Beatmap.RulesetID > 3) + if (score.ScoreInfo.BeatmapInfo.RulesetID < 0 || score.ScoreInfo.BeatmapInfo.RulesetID > 3) throw new ArgumentException("Only scores in the osu, taiko, catch, or mania rulesets can be encoded to the legacy score format.", nameof(score)); } @@ -44,7 +44,7 @@ namespace osu.Game.Scoring.Legacy { sw.Write((byte)(score.ScoreInfo.Ruleset.ID ?? 0)); sw.Write(LATEST_VERSION); - sw.Write(score.ScoreInfo.Beatmap.MD5Hash); + sw.Write(score.ScoreInfo.BeatmapInfo.MD5Hash); sw.Write(score.ScoreInfo.UserString); sw.Write($"lazer-{score.ScoreInfo.UserString}-{score.ScoreInfo.Date}".ComputeMD5Hash()); sw.Write((ushort)(score.ScoreInfo.GetCount300() ?? 0)); diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 890ead40e3..5cf22f7945 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -150,7 +150,8 @@ namespace osu.Game.Scoring public int BeatmapInfoID { get; set; } [JsonIgnore] - public virtual BeatmapInfo Beatmap { get; set; } + [Column("Beatmap")] + public virtual BeatmapInfo BeatmapInfo { get; set; } [JsonIgnore] public long? OnlineScoreID { get; set; } @@ -252,7 +253,7 @@ namespace osu.Game.Scoring return clone; } - public override string ToString() => $"{User} playing {Beatmap}"; + public override string ToString() => $"{User} playing {BeatmapInfo}"; public bool Equals(ScoreInfo other) { diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index d83b4e3f1d..27d087dc30 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -67,7 +67,7 @@ namespace osu.Game.Scoring // Compute difficulties asynchronously first to prevent blocking via the GetTotalScore() call below. foreach (var s in scores) { - await difficultyCache.GetDifficultyAsync(s.Beatmap, s.Ruleset, s.Mods, cancellationToken).ConfigureAwait(false); + await difficultyCache.GetDifficultyAsync(s.BeatmapInfo, s.Ruleset, s.Mods, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } } @@ -126,7 +126,7 @@ namespace osu.Game.Scoring /// The total score. public async Task GetTotalScoreAsync([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default) { - if (score.Beatmap == null) + if (score.BeatmapInfo == null) return score.TotalScore; int beatmapMaxCombo; @@ -147,18 +147,18 @@ namespace osu.Game.Scoring // This score is guaranteed to be an osu!stable score. // The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used. - if (score.Beatmap.MaxCombo != null) - beatmapMaxCombo = score.Beatmap.MaxCombo.Value; + if (score.BeatmapInfo.MaxCombo != null) + beatmapMaxCombo = score.BeatmapInfo.MaxCombo.Value; else { - if (score.Beatmap.ID == 0 || difficulties == null) + if (score.BeatmapInfo.ID == 0 || difficulties == null) { // We don't have enough information (max combo) to compute the score, so use the provided score. return score.TotalScore; } // We can compute the max combo locally after the async beatmap difficulty computation. - var difficulty = await difficulties().GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false); + var difficulty = await difficulties().GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false); beatmapMaxCombo = difficulty.MaxCombo; } } diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs index bb15983de3..82685e9a04 100644 --- a/osu.Game/Scoring/ScorePerformanceCache.cs +++ b/osu.Game/Scoring/ScorePerformanceCache.cs @@ -34,7 +34,7 @@ namespace osu.Game.Scoring { var score = lookup.ScoreInfo; - var attributes = await difficultyCache.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token).ConfigureAwait(false); + var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, token).ConfigureAwait(false); // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. if (attributes.Attributes == null) diff --git a/osu.Game/Scoring/ScoreStore.cs b/osu.Game/Scoring/ScoreStore.cs index f5c5cd5dad..fd1f5ae3ec 100644 --- a/osu.Game/Scoring/ScoreStore.cs +++ b/osu.Game/Scoring/ScoreStore.cs @@ -17,9 +17,9 @@ namespace osu.Game.Scoring protected override IQueryable AddIncludesForConsumption(IQueryable query) => base.AddIncludesForConsumption(query) - .Include(s => s.Beatmap) - .Include(s => s.Beatmap).ThenInclude(b => b.Metadata) - .Include(s => s.Beatmap).ThenInclude(b => b.BeatmapSet).ThenInclude(s => s.Metadata) + .Include(s => s.BeatmapInfo) + .Include(s => s.BeatmapInfo).ThenInclude(b => b.Metadata) + .Include(s => s.BeatmapInfo).ThenInclude(b => b.BeatmapSet).ThenInclude(s => s.Metadata) .Include(s => s.Ruleset); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs index 95ccc08608..c3190cd845 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Spectate/PlayerArea.cs @@ -84,7 +84,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate Score = score; - gameplayContent.Child = new PlayerIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.Beatmap), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) + gameplayContent.Child = new PlayerIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.BeatmapInfo), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods) { RelativeSizeAxes = Axes.Both, Child = stack = new OsuScreenStack() diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a05a8f5056..69a1c6c8ce 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -164,7 +164,7 @@ namespace osu.Game.Screens.Play Score = CreateScore(); // ensure the score is in a consistent state with the current player. - Score.ScoreInfo.Beatmap = Beatmap.Value.BeatmapInfo; + Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo; Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; Score.ScoreInfo.Mods = Mods.Value.ToArray(); diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs index d90e8e0168..675cb71311 100644 --- a/osu.Game/Screens/Play/SoloPlayer.cs +++ b/osu.Game/Screens/Play/SoloPlayer.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Play protected override APIRequest CreateSubmissionRequest(Score score, long token) { - var beatmap = score.ScoreInfo.Beatmap; + var beatmap = score.ScoreInfo.BeatmapInfo; Debug.Assert(beatmap.OnlineBeatmapID != null); diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index bcb5e7999f..262d1e8293 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.Ranking.Expanded [BackgroundDependencyLoader] private void load(BeatmapDifficultyCache beatmapDifficultyCache) { - var beatmap = score.Beatmap; + var beatmap = score.BeatmapInfo; var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata; var creator = metadata.Author?.Username; diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 9bc696948f..5e582a8dcb 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -27,10 +27,10 @@ namespace osu.Game.Screens.Ranking protected override APIRequest FetchScores(Action> scoresCallback) { - if (Score.Beatmap.OnlineBeatmapID == null || Score.Beatmap.Status <= BeatmapSetOnlineStatus.Pending) + if (Score.BeatmapInfo.OnlineBeatmapID == null || Score.BeatmapInfo.Status <= BeatmapSetOnlineStatus.Pending) return null; - getScoreRequest = new GetScoresRequest(Score.Beatmap, Score.Ruleset); + getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineScoreID != Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets))); return getScoreRequest; } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index f1ae1f9d73..bc62bcf2b2 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -102,7 +102,7 @@ namespace osu.Game.Screens.Ranking.Statistics // Todo: The placement of this is temporary. Eventually we'll both generate the playable beatmap _and_ run through it in a background task to generate the hit events. Task.Run(() => { - playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.Beatmap).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods ?? Array.Empty()); + playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods ?? Array.Empty()); }, loadCancellation.Token).ContinueWith(t => Schedule(() => { var rows = new FillFlowContainer diff --git a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs index 8c33b1ea0b..4970db8955 100644 --- a/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs +++ b/osu.Game/Screens/Select/BeatmapClearScoresDialog.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Select Text = @"Yes. Please.", Action = () => { - Task.Run(() => scoreManager.Delete(scoreManager.QueryScores(s => !s.DeletePending && s.Beatmap.ID == beatmapInfo.ID).ToList())) + Task.Run(() => scoreManager.Delete(scoreManager.QueryScores(s => !s.DeletePending && s.BeatmapInfo.ID == beatmapInfo.ID).ToList())) .ContinueWith(_ => onCompletion); } }, diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 2fdb41a1a1..07300635aa 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -141,7 +141,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (Scope == BeatmapLeaderboardScope.Local) { var scores = scoreManager - .QueryScores(s => !s.DeletePending && s.Beatmap.ID == BeatmapInfo.ID && s.Ruleset.ID == ruleset.Value.ID); + .QueryScores(s => !s.DeletePending && s.BeatmapInfo.ID == BeatmapInfo.ID && s.Ruleset.ID == ruleset.Value.ID); if (filterMods && !mods.Value.Any()) { diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 418cf23ce7..94aa165785 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Select } protected void PresentScore(ScoreInfo score) => - FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new SoloResultsScreen(score, false))); + FinaliseSelection(score.BeatmapInfo, score.Ruleset, () => this.Push(new SoloResultsScreen(score, false))); protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); diff --git a/osu.Game/Screens/Spectate/SpectatorScreen.cs b/osu.Game/Screens/Spectate/SpectatorScreen.cs index 71bcc336f3..7861d4cb72 100644 --- a/osu.Game/Screens/Spectate/SpectatorScreen.cs +++ b/osu.Game/Screens/Spectate/SpectatorScreen.cs @@ -165,7 +165,7 @@ namespace osu.Game.Screens.Spectate { ScoreInfo = new ScoreInfo { - Beatmap = resolvedBeatmap, + BeatmapInfo = resolvedBeatmap, User = user, Mods = spectatorState.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(), Ruleset = resolvedRuleset.RulesetInfo, diff --git a/osu.Game/Tests/TestScoreInfo.cs b/osu.Game/Tests/TestScoreInfo.cs index 5ce6aae647..719d31b092 100644 --- a/osu.Game/Tests/TestScoreInfo.cs +++ b/osu.Game/Tests/TestScoreInfo.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }; - Beatmap = new TestBeatmap(ruleset).BeatmapInfo; + BeatmapInfo = new TestBeatmap(ruleset).BeatmapInfo; Ruleset = ruleset; RulesetID = ruleset.ID ?? 0; From 2e3450b3f58e4290289194c0fd58ecf28fe3931c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 20:20:24 +0900 Subject: [PATCH 194/391] Make Mods readonly --- osu.Game/Screens/Play/GameplayState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index ba08c946d2..d4da17ce37 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Play /// /// The mods applied to the gameplay. /// - public IReadOnlyList Mods; + public readonly IReadOnlyList Mods; /// /// A bindable tracking the last judgement result applied to any hit object. From 5aae673240e2263cb15d2b9e7c1ad055d17ecaf9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 20:33:54 +0900 Subject: [PATCH 195/391] Use GameplayState --- osu.Game/Screens/Play/GameplayState.cs | 6 +++++ .../HUD/DefaultPerformancePointsCounter.cs | 22 +++++++++---------- osu.Game/Screens/Play/Player.cs | 2 +- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index d4da17ce37..ef4967b34d 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -7,6 +7,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; #nullable enable @@ -32,6 +33,11 @@ namespace osu.Game.Screens.Play /// public readonly IReadOnlyList Mods; + /// + /// The gameplay score. + /// + public Score? Score { get; set; } = null; + /// /// A bindable tracking the last judgement result applied to any hit object. /// diff --git a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs index d93d626c72..3c31848c32 100644 --- a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs @@ -36,9 +36,9 @@ namespace osu.Game.Screens.Play.HUD [Resolved(CanBeNull = true)] private ScoreProcessor scoreProcessor { get; set; } + [Resolved] [CanBeNull] - [Resolved(CanBeNull = true)] - private Player player { get; set; } + private GameplayState gameplayState { get; set; } private TimedDifficultyAttributes[] timedAttributes; private Ruleset gameplayRuleset; @@ -53,10 +53,10 @@ namespace osu.Game.Screens.Play.HUD { Colour = colours.BlueLighter; - if (player != null) + if (gameplayState != null) { - gameplayRuleset = player.GameplayRuleset; - timedAttributes = gameplayRuleset.CreateDifficultyCalculator(new GameplayWorkingBeatmap(player.GameplayBeatmap)).CalculateTimed(player.Mods.Value.ToArray()).ToArray(); + gameplayRuleset = gameplayState.Ruleset; + timedAttributes = gameplayRuleset.CreateDifficultyCalculator(new GameplayWorkingBeatmap(gameplayState.Beatmap)).CalculateTimed(gameplayState.Mods.ToArray()).ToArray(); } } @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Play.HUD private void onNewJudgement(JudgementResult judgement) { - if (player == null || timedAttributes.Length == 0) + if (gameplayState?.Score == null || timedAttributes.Length == 0) return; var attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Play.HUD attribIndex = ~attribIndex - 1; attribIndex = Math.Clamp(attribIndex, 0, timedAttributes.Length - 1); - var ppProcessor = gameplayRuleset.CreatePerformanceCalculator(timedAttributes[attribIndex].Attributes, player.Score.ScoreInfo); + var ppProcessor = gameplayRuleset.CreatePerformanceCalculator(timedAttributes[attribIndex].Attributes, gameplayState.Score.ScoreInfo); Current.Value = (int)(ppProcessor?.Calculate() ?? 0); } @@ -134,18 +134,18 @@ namespace osu.Game.Screens.Play.HUD private class GameplayWorkingBeatmap : WorkingBeatmap { - private readonly GameplayBeatmap gameplayBeatmap; + private readonly IBeatmap gameplayBeatmap; - public GameplayWorkingBeatmap(GameplayBeatmap gameplayBeatmap) + public GameplayWorkingBeatmap(IBeatmap gameplayBeatmap) : base(gameplayBeatmap.BeatmapInfo, null) { this.gameplayBeatmap = gameplayBeatmap; } public override IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null) - => gameplayBeatmap.PlayableBeatmap; + => gameplayBeatmap; - protected override IBeatmap GetBeatmap() => gameplayBeatmap.PlayableBeatmap; + protected override IBeatmap GetBeatmap() => gameplayBeatmap; protected override Texture GetBackground() => throw new NotImplementedException(); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 15cf8388f0..00907584e1 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -127,7 +127,7 @@ namespace osu.Game.Screens.Play [Cached] [Cached(Type = typeof(IBindable>))] - public new readonly Bindable> Mods = new Bindable>(Array.Empty()); + protected new readonly Bindable> Mods = new Bindable>(Array.Empty()); /// /// Whether failing should be allowed. From 221cc1747c1ec62e05cf6288cda88f876f31f58d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 20:34:08 +0900 Subject: [PATCH 196/391] Drop "default" prefix --- ...erformancePointsCounter.cs => PerformancePointsCounter.cs} | 4 ++-- osu.Game/Skinning/DefaultSkin.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename osu.Game/Screens/Play/HUD/{DefaultPerformancePointsCounter.cs => PerformancePointsCounter.cs} (97%) diff --git a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs similarity index 97% rename from osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs rename to osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 3c31848c32..ad0a928d1c 100644 --- a/osu.Game/Screens/Play/HUD/DefaultPerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -28,7 +28,7 @@ using osuTK; namespace osu.Game.Screens.Play.HUD { - public class DefaultPerformancePointsCounter : RollingCounter, ISkinnableDrawable + public class PerformancePointsCounter : RollingCounter, ISkinnableDrawable { public bool UsesFixedAnchor { get; set; } @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Play.HUD private TimedDifficultyAttributes[] timedAttributes; private Ruleset gameplayRuleset; - public DefaultPerformancePointsCounter() + public PerformancePointsCounter() { Current.Value = DisplayedCount = 0; } diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 41b7875cd1..8c1e5313d5 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -68,7 +68,7 @@ namespace osu.Game.Skinning var score = container.OfType().FirstOrDefault(); var accuracy = container.OfType().FirstOrDefault(); var combo = container.OfType().FirstOrDefault(); - var ppCounter = container.OfType().FirstOrDefault(); + var ppCounter = container.OfType().FirstOrDefault(); if (score != null) { @@ -131,7 +131,7 @@ namespace osu.Game.Skinning new SongProgress(), new BarHitErrorMeter(), new BarHitErrorMeter(), - new DefaultPerformancePointsCounter() + new PerformancePointsCounter() } }; From fb63e5ed87fab819899e8a137d0bf7b67ce720c8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 20:35:26 +0900 Subject: [PATCH 197/391] Add todo --- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index ad0a928d1c..740f286246 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -132,6 +132,7 @@ namespace osu.Game.Screens.Play.HUD } } + // Todo: This class shouldn't exist, but requires breaking changes to allow DifficultyCalculator to receive an IBeatmap. private class GameplayWorkingBeatmap : WorkingBeatmap { private readonly IBeatmap gameplayBeatmap; From 1837e1bf3ccb4e471979e1034c3ee92362d49739 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 20:35:53 +0900 Subject: [PATCH 198/391] Share rounding with PerformanceStatistic --- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 740f286246..265c1a9d01 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -79,7 +79,7 @@ namespace osu.Game.Screens.Play.HUD attribIndex = Math.Clamp(attribIndex, 0, timedAttributes.Length - 1); var ppProcessor = gameplayRuleset.CreatePerformanceCalculator(timedAttributes[attribIndex].Attributes, gameplayState.Score.ScoreInfo); - Current.Value = (int)(ppProcessor?.Calculate() ?? 0); + Current.Value = (int)Math.Round(ppProcessor?.Calculate() ?? 0, MidpointRounding.AwayFromZero); } protected override LocalisableString FormatCount(int count) => count.ToString(@"D"); From 0b0316e27ee68696d090cb4410f13e9aa0117045 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 20:59:31 +0900 Subject: [PATCH 199/391] Fix missing CanBeNull --- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 265c1a9d01..154b6accb8 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.Play.HUD [Resolved(CanBeNull = true)] private ScoreProcessor scoreProcessor { get; set; } - [Resolved] + [Resolved(CanBeNull = true)] [CanBeNull] private GameplayState gameplayState { get; set; } From d1e7191f94fa1de0d3e7911951a5cb9179125a17 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 20:59:51 +0900 Subject: [PATCH 200/391] Pass score into GameplayState --- osu.Game/Screens/Play/Player.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 00907584e1..69093db883 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -162,6 +162,7 @@ namespace osu.Game.Screens.Play return; Score = CreateScore(); + GameplayState.Score = Score; // ensure the score is in a consistent state with the current player. Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo; From d120678e306e9dc85cfb0e8d2fd82efd96c3a7cb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 4 Oct 2021 21:13:14 +0900 Subject: [PATCH 201/391] Fix redundant default value --- osu.Game/Screens/Play/GameplayState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index ef4967b34d..9c83eddb45 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.Play /// /// The gameplay score. /// - public Score? Score { get; set; } = null; + public Score? Score { get; set; } /// /// A bindable tracking the last judgement result applied to any hit object. From 15ec315ec62a9b32e9d6a81fcd9a2ddffbcabf9c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 01:14:58 +0900 Subject: [PATCH 202/391] Fix test runs hanging due to missing `ConfigureAwait` --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index d1c23f1442..4cc71717ff 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -909,7 +909,7 @@ namespace osu.Game.Tests.Beatmaps.IO var manager = osu.Dependencies.Get(); - var importedSet = await manager.Import(new ImportTask(temp)); + var importedSet = await manager.Import(new ImportTask(temp)).ConfigureAwait(false); ensureLoaded(osu); @@ -924,7 +924,7 @@ namespace osu.Game.Tests.Beatmaps.IO var manager = osu.Dependencies.Get(); - var importedSet = await manager.Import(new ImportTask(temp)); + var importedSet = await manager.Import(new ImportTask(temp)).ConfigureAwait(false); ensureLoaded(osu); From 86df4919f7ab8207e62d9b49cdcd06bbb0c6e6c5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 5 Oct 2021 11:06:24 +0900 Subject: [PATCH 203/391] Fix skin fallbacks test --- osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs index 7d4673c901..7398527f57 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneBeatmapSkinFallbacks.cs @@ -42,6 +42,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestEmptyLegacyBeatmapSkinFallsBack() { CreateSkinTest(SkinInfo.Default, () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null)); + AddUntilStep("wait for hud load", () => Player.ChildrenOfType().All(c => c.ComponentsLoaded)); AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value)); } From 593da79bbc96bd18fe6bf0eb35c5758fb1c16153 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 5 Oct 2021 11:26:13 +0900 Subject: [PATCH 204/391] Further asyncify load process --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 9 +++++++++ .../Screens/Play/HUD/PerformancePointsCounter.cs | 15 ++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index c46ab93ece..e6c287112f 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -17,6 +17,7 @@ using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -147,6 +148,14 @@ namespace osu.Game.Beatmaps }, token, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); } + public Task GetTimedDifficultyAttributesAsync(WorkingBeatmap beatmap, Ruleset ruleset, Mod[] mods, CancellationToken token = default) + { + return Task.Factory.StartNew(() => ruleset.CreateDifficultyCalculator(beatmap).CalculateTimed(mods).ToArray(), + token, + TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, + updateScheduler); + } + /// /// Retrieves the that describes a star rating. /// diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 154b6accb8..b02ab440a0 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -3,8 +3,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio.Track; @@ -40,7 +43,10 @@ namespace osu.Game.Screens.Play.HUD [CanBeNull] private GameplayState gameplayState { get; set; } + [CanBeNull] private TimedDifficultyAttributes[] timedAttributes; + + [CanBeNull] private Ruleset gameplayRuleset; public PerformancePointsCounter() @@ -49,14 +55,15 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, BeatmapDifficultyCache difficultyCache, CancellationToken cancellationToken) { Colour = colours.BlueLighter; if (gameplayState != null) { gameplayRuleset = gameplayState.Ruleset; - timedAttributes = gameplayRuleset.CreateDifficultyCalculator(new GameplayWorkingBeatmap(gameplayState.Beatmap)).CalculateTimed(gameplayState.Mods.ToArray()).ToArray(); + difficultyCache.GetTimedDifficultyAttributesAsync(new GameplayWorkingBeatmap(gameplayState.Beatmap), gameplayRuleset, gameplayState.Mods.ToArray(), cancellationToken) + .ContinueWith(r => Schedule(() => timedAttributes = r.Result), TaskContinuationOptions.OnlyOnRanToCompletion); } } @@ -70,9 +77,11 @@ namespace osu.Game.Screens.Play.HUD private void onNewJudgement(JudgementResult judgement) { - if (gameplayState?.Score == null || timedAttributes.Length == 0) + if (gameplayState?.Score == null || timedAttributes == null || timedAttributes.Length == 0) return; + Debug.Assert(gameplayRuleset != null); + var attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); if (attribIndex < 0) attribIndex = ~attribIndex - 1; From 5624dd9af67376b73038e4f75570927f161ff602 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 5 Oct 2021 12:07:41 +0900 Subject: [PATCH 205/391] Fix incorrect CancellationToken usage Apparently I wrote the BDL system and don't know how this works. I believe you need `CancellationToken?` or CanBeNull=true, however that doesn't actually play well when actually using the token in code... --- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index b02ab440a0..13b94e6cd6 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -49,20 +49,22 @@ namespace osu.Game.Screens.Play.HUD [CanBeNull] private Ruleset gameplayRuleset; + private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource(); + public PerformancePointsCounter() { Current.Value = DisplayedCount = 0; } [BackgroundDependencyLoader] - private void load(OsuColour colours, BeatmapDifficultyCache difficultyCache, CancellationToken cancellationToken) + private void load(OsuColour colours, BeatmapDifficultyCache difficultyCache) { Colour = colours.BlueLighter; if (gameplayState != null) { gameplayRuleset = gameplayState.Ruleset; - difficultyCache.GetTimedDifficultyAttributesAsync(new GameplayWorkingBeatmap(gameplayState.Beatmap), gameplayRuleset, gameplayState.Mods.ToArray(), cancellationToken) + difficultyCache.GetTimedDifficultyAttributesAsync(new GameplayWorkingBeatmap(gameplayState.Beatmap), gameplayRuleset, gameplayState.Mods.ToArray(), loadCancellationSource.Token) .ContinueWith(r => Schedule(() => timedAttributes = r.Result), TaskContinuationOptions.OnlyOnRanToCompletion); } } @@ -101,6 +103,8 @@ namespace osu.Game.Screens.Play.HUD if (scoreProcessor != null) scoreProcessor.NewJudgement -= onNewJudgement; + + loadCancellationSource?.Cancel(); } private class TextComponent : CompositeDrawable, IHasText From b41fa41c8539d6370fb4d3efd8539ec664176ff2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 14:28:56 +0900 Subject: [PATCH 206/391] Rename `APIRequest.Result` to `Response` --- .../Online/TestDummyAPIRequestHandling.cs | 2 +- .../TestSceneUpdateableBeatmapBackgroundSprite.cs | 4 ++-- osu.Game.Tournament/TournamentGameBase.cs | 4 ++-- osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs | 2 +- osu.Game/Database/UserLookupCache.cs | 2 +- osu.Game/Online/API/APIRequest.cs | 15 ++++++++++----- osu.Game/Overlays/RankingsOverlay.cs | 6 +++--- .../Screens/OnlinePlay/Components/RoomManager.cs | 2 +- 8 files changed, 21 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs index aa29d76843..91c6b6c008 100644 --- a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs +++ b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Online AddAssert("response event fired", () => response != null); - AddAssert("request has response", () => request.Result == response); + AddAssert("request has response", () => request.Response == response); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs index 198cc70e01..74cd675a05 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneUpdateableBeatmapBackgroundSprite.cs @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.UserInterface var req = new GetBeatmapSetRequest(1); api.Queue(req); - AddUntilStep("wait for api response", () => req.Result != null); + AddUntilStep("wait for api response", () => req.Response != null); TestUpdateableBeatmapBackgroundSprite background = null; @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.UserInterface Child = background = new TestUpdateableBeatmapBackgroundSprite { RelativeSizeAxes = Axes.Both, - Beatmap = { Value = new BeatmapInfo { BeatmapSet = req.Result?.ToBeatmapSet(rulesets) } } + Beatmap = { Value = new BeatmapInfo { BeatmapSet = req.Response?.ToBeatmapSet(rulesets) } } }; }); diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 2e4ed9d5b1..bdf7269c83 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -182,7 +182,7 @@ namespace osu.Game.Tournament { var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID }); API.Perform(req); - b.BeatmapInfo = req.Result?.ToBeatmapInfo(RulesetStore); + b.BeatmapInfo = req.Response?.ToBeatmapInfo(RulesetStore); addedInfo = true; } @@ -203,7 +203,7 @@ namespace osu.Game.Tournament { var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = b.ID }); req.Perform(API); - b.BeatmapInfo = req.Result?.ToBeatmapInfo(RulesetStore); + b.BeatmapInfo = req.Response?.ToBeatmapInfo(RulesetStore); addedInfo = true; } diff --git a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs index e1faf6005b..1fe120557d 100644 --- a/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapOnlineLookupQueue.cs @@ -78,7 +78,7 @@ namespace osu.Game.Beatmaps // intentionally blocking to limit web request concurrency api.Perform(req); - var res = req.Result; + var res = req.Response; if (res != null) { diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index 13c37ddfe9..ff81637efb 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -115,7 +115,7 @@ namespace osu.Game.Database createNewTask(); } - List foundUsers = request.Result?.Users; + List foundUsers = request.Response?.Users; if (foundUsers != null) { diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index cf17ed4b5d..d60c9cfe65 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using JetBrains.Annotations; using Newtonsoft.Json; using osu.Framework.IO.Network; using osu.Framework.Logging; @@ -17,7 +18,11 @@ namespace osu.Game.Online.API { protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest(Uri); - public T Result { get; private set; } + /// + /// The deserialised response object. May be null if the request or deserialisation failed. + /// + [CanBeNull] + public T Response { get; private set; } /// /// Invoked on successful completion of an API request. @@ -27,21 +32,21 @@ namespace osu.Game.Online.API protected APIRequest() { - base.Success += () => Success?.Invoke(Result); + base.Success += () => Success?.Invoke(Response); } protected override void PostProcess() { base.PostProcess(); - Result = ((OsuJsonWebRequest)WebRequest)?.ResponseObject; + Response = ((OsuJsonWebRequest)WebRequest)?.ResponseObject; } internal void TriggerSuccess(T result) { - if (Result != null) + if (Response != null) throw new InvalidOperationException("Attempted to trigger success more than once"); - Result = result; + Response = result; TriggerSuccess(); } diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index b8bdef925e..2263d54d7b 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -146,16 +146,16 @@ namespace osu.Game.Overlays switch (userRequest.Type) { case UserRankingsType.Performance: - return new PerformanceTable(1, userRequest.Result.Users); + return new PerformanceTable(1, userRequest.Response.Users); case UserRankingsType.Score: - return new ScoresTable(1, userRequest.Result.Users); + return new ScoresTable(1, userRequest.Response.Users); } return null; case GetCountryRankingsRequest countryRequest: - return new CountriesTable(1, countryRequest.Result.Countries); + return new CountriesTable(1, countryRequest.Response.Countries); } return null; diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index a64d89b699..381849189d 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.OnlinePlay.Components req.Failure += exception => { - onError?.Invoke(req.Result?.Error ?? exception.Message); + onError?.Invoke(req.Response?.Error ?? exception.Message); }; api.Queue(req); From d3b9660148d3ebc79660915c1db35b779b001402 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 14:41:14 +0900 Subject: [PATCH 207/391] Move common interface implementations to extension methods --- .../BeatmapMetadataRomanisationTest.cs | 4 +- osu.Game/Beatmaps/BeatmapInfo.cs | 7 +-- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 37 +++++++++++++++ osu.Game/Beatmaps/BeatmapMetadata.cs | 7 +-- .../Beatmaps/BeatmapMetadataInfoExtensions.cs | 46 +++++++++++++++++++ osu.Game/Beatmaps/IBeatmapInfo.cs | 30 ------------ osu.Game/Beatmaps/IBeatmapMetadataInfo.cs | 43 ----------------- osu.Game/Overlays/Music/PlaylistItem.cs | 2 +- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 2 +- .../Lounge/Components/DrawableRoom.cs | 2 +- .../Select/Carousel/CarouselBeatmap.cs | 2 +- 11 files changed, 91 insertions(+), 91 deletions(-) create mode 100644 osu.Game/Beatmaps/BeatmapInfoExtensions.cs create mode 100644 osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs diff --git a/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs b/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs index dab4825919..9926acf772 100644 --- a/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs +++ b/osu.Game.Tests/Localisation/BeatmapMetadataRomanisationTest.cs @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Localisation Title = "Romanised title", TitleUnicode = "Unicode Title" }; - var romanisableString = metadata.ToRomanisableString(); + var romanisableString = metadata.GetDisplayTitleRomanisable(); Assert.AreEqual(metadata.ToString(), romanisableString.Romanised); Assert.AreEqual($"{metadata.ArtistUnicode} - {metadata.TitleUnicode}", romanisableString.Original); @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Localisation Artist = "Romanised Artist", Title = "Romanised title" }; - var romanisableString = metadata.ToRomanisableString(); + var romanisableString = metadata.GetDisplayTitleRomanisable(); Assert.AreEqual(romanisableString.Romanised, romanisableString.Original); } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index cd5f8fb9a1..ac5b5d7a8a 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -7,7 +7,6 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Newtonsoft.Json; -using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Rulesets; @@ -152,11 +151,7 @@ namespace osu.Game.Beatmaps [JsonIgnore] public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarDifficulty); - public IEnumerable SearchableTerms => ((IBeatmapInfo)this).SearchableTerms; - - public override string ToString() => ((IBeatmapInfo)this).DisplayTitle; - - public RomanisableString ToRomanisableString() => ((IBeatmapInfo)this).DisplayTitleRomanisable; + public override string ToString() => this.GetDisplayTitle(); public bool Equals(BeatmapInfo other) { diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs new file mode 100644 index 0000000000..deab8b915a --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.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.Linq; +using osu.Framework.Localisation; + +namespace osu.Game.Beatmaps +{ + public static class BeatmapInfoExtensions + { + /// + /// A user-presentable display title representing this beatmap. + /// + public static string GetDisplayTitle(this IBeatmapInfo beatmapInfo) => $"{getClosestMetadata(beatmapInfo)} {getVersionString(beatmapInfo)}".Trim(); + + /// + /// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields. + /// + public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapInfo beatmapInfo) + { + var metadata = getClosestMetadata(beatmapInfo).GetDisplayTitleRomanisable(); + var versionString = getVersionString(beatmapInfo); + + return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim()); + } + + public static string[] GetSearchableTerms(this IBeatmapInfo beatmapInfo) => new[] + { + beatmapInfo.DifficultyName + }.Concat(getClosestMetadata(beatmapInfo).GetSearchableTerms()).Where(s => !string.IsNullOrEmpty(s)).ToArray(); + + private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]"; + + // temporary helper methods until we figure which metadata should be where. + private static IBeatmapMetadataInfo getClosestMetadata(IBeatmapInfo beatmapInfo) => (beatmapInfo.Metadata ?? beatmapInfo.BeatmapSet.Metadata)!; + } +} diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index 3da80580cb..711533e118 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using Newtonsoft.Json; -using osu.Framework.Localisation; using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Users; @@ -87,11 +86,7 @@ namespace osu.Game.Beatmaps public bool Equals(BeatmapMetadata other) => ((IBeatmapMetadataInfo)this).Equals(other); - public override string ToString() => ((IBeatmapMetadataInfo)this).DisplayTitle; - - public RomanisableString ToRomanisableString() => ((IBeatmapMetadataInfo)this).DisplayTitleRomanisable; - - public IEnumerable SearchableTerms => ((IBeatmapMetadataInfo)this).SearchableTerms; + public override string ToString() => this.GetDisplayTitle(); string IBeatmapMetadataInfo.Author => AuthorString; } diff --git a/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.cs new file mode 100644 index 0000000000..ee946eeeec --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapMetadataInfoExtensions.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 osu.Framework.Localisation; + +namespace osu.Game.Beatmaps +{ + public static class BeatmapMetadataInfoExtensions + { + /// + /// An array of all searchable terms provided in contained metadata. + /// + public static string[] GetSearchableTerms(this IBeatmapMetadataInfo metadataInfo) => new[] + { + metadataInfo.Author, + metadataInfo.Artist, + metadataInfo.ArtistUnicode, + metadataInfo.Title, + metadataInfo.TitleUnicode, + metadataInfo.Source, + metadataInfo.Tags + }.Where(s => !string.IsNullOrEmpty(s)).ToArray(); + + /// + /// A user-presentable display title representing this metadata. + /// + public static string GetDisplayTitle(this IBeatmapMetadataInfo metadataInfo) + { + string author = string.IsNullOrEmpty(metadataInfo.Author) ? string.Empty : $"({metadataInfo.Author})"; + return $"{metadataInfo.Artist} - {metadataInfo.Title} {author}".Trim(); + } + + /// + /// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields. + /// + public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapMetadataInfo metadataInfo) + { + string author = string.IsNullOrEmpty(metadataInfo.Author) ? string.Empty : $"({metadataInfo.Author})"; + var artistUnicode = string.IsNullOrEmpty(metadataInfo.ArtistUnicode) ? metadataInfo.Artist : metadataInfo.ArtistUnicode; + var titleUnicode = string.IsNullOrEmpty(metadataInfo.TitleUnicode) ? metadataInfo.Title : metadataInfo.TitleUnicode; + + return new RomanisableString($"{artistUnicode} - {titleUnicode} {author}".Trim(), $"{metadataInfo.Artist} - {metadataInfo.Title} {author}".Trim()); + } + } +} diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs index fb30b0279c..6a3f1b43d8 100644 --- a/osu.Game/Beatmaps/IBeatmapInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapInfo.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.Linq; -using osu.Framework.Localisation; using osu.Game.Database; using osu.Game.Rulesets; @@ -64,33 +62,5 @@ namespace osu.Game.Beatmaps /// The basic star rating for this beatmap (with no mods applied). /// double StarRating { get; } - - /// - /// A user-presentable display title representing this metadata. - /// - string DisplayTitle => $"{Metadata} {versionString}".Trim(); - - /// - /// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields. - /// - RomanisableString DisplayTitleRomanisable - { - get - { - var metadata = closestMetadata.DisplayTitleRomanisable; - - return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim()); - } - } - - string[] SearchableTerms => new[] - { - DifficultyName - }.Concat(closestMetadata.SearchableTerms).Where(s => !string.IsNullOrEmpty(s)).ToArray(); - - private string versionString => string.IsNullOrEmpty(DifficultyName) ? string.Empty : $"[{DifficultyName}]"; - - // temporary helper methods until we figure which metadata should be where. - private IBeatmapMetadataInfo closestMetadata => (Metadata ?? BeatmapSet.Metadata)!; } } diff --git a/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs b/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs index d0dae296a0..55aee7d7bc 100644 --- a/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapMetadataInfo.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; -using osu.Framework.Localisation; #nullable enable @@ -65,47 +63,6 @@ namespace osu.Game.Beatmaps /// string BackgroundFile { get; } - /// - /// A user-presentable display title representing this metadata. - /// - string DisplayTitle - { - get - { - string author = string.IsNullOrEmpty(Author) ? string.Empty : $"({Author})"; - return $"{Artist} - {Title} {author}".Trim(); - } - } - - /// - /// A user-presentable display title representing this metadata, with localisation handling for potentially romanisable fields. - /// - RomanisableString DisplayTitleRomanisable - { - get - { - string author = string.IsNullOrEmpty(Author) ? string.Empty : $"({Author})"; - var artistUnicode = string.IsNullOrEmpty(ArtistUnicode) ? Artist : ArtistUnicode; - var titleUnicode = string.IsNullOrEmpty(TitleUnicode) ? Title : TitleUnicode; - - return new RomanisableString($"{artistUnicode} - {titleUnicode} {author}".Trim(), $"{Artist} - {Title} {author}".Trim()); - } - } - - /// - /// An array of all searchable terms provided in contained metadata. - /// - string[] SearchableTerms => new[] - { - Author, - Artist, - ArtistUnicode, - Title, - TitleUnicode, - Source, - Tags - }.Where(s => !string.IsNullOrEmpty(s)).ToArray(); - bool IEquatable.Equals(IBeatmapMetadataInfo? other) { if (other == null) diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 571b14428e..ef25de77c6 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -38,7 +38,7 @@ namespace osu.Game.Overlays.Music { Padding = new MarginPadding { Left = 5 }; - FilterTerms = item.Metadata.SearchableTerms; + FilterTerms = item.Metadata.GetSearchableTerms(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 69eb857661..585b024623 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value, requiredMods) { Size = new Vector2(32) }; beatmapText.Clear(); - beatmapText.AddLink(Item.Beatmap.Value.ToRomanisableString(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString(), null, text => + beatmapText.AddLink(Item.Beatmap.Value.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString(), null, text => { text.Truncate = true; text.RelativeSizeAxes = Axes.X; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 03d13c353a..acd87ed864 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -379,7 +379,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components if (item.NewValue?.Beatmap.Value != null) { statusText.Text = "Currently playing "; - beatmapText.AddLink(item.NewValue.Beatmap.Value.ToRomanisableString(), + beatmapText.AddLink(item.NewValue.Beatmap.Value.GetDisplayTitleRomanisable(), LinkAction.OpenBeatmap, item.NewValue.Beatmap.Value.OnlineBeatmapID.ToString(), creationParameters: s => diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 3f729d9477..d8c5aa760e 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Select.Carousel if (match) { - var terms = BeatmapInfo.SearchableTerms; + var terms = BeatmapInfo.GetSearchableTerms(); foreach (var criteriaTerm in criteria.SearchTerms) match &= terms.Any(term => term.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase)); From a5aa32811aaf215f7d96d601a82d44362d140ad3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 14:49:59 +0900 Subject: [PATCH 208/391] Remove null check suppression and add non-null fallback --- osu.Game/Beatmaps/BeatmapInfoExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs index deab8b915a..eba19ac1a1 100644 --- a/osu.Game/Beatmaps/BeatmapInfoExtensions.cs +++ b/osu.Game/Beatmaps/BeatmapInfoExtensions.cs @@ -32,6 +32,7 @@ namespace osu.Game.Beatmaps private static string getVersionString(IBeatmapInfo beatmapInfo) => string.IsNullOrEmpty(beatmapInfo.DifficultyName) ? string.Empty : $"[{beatmapInfo.DifficultyName}]"; // temporary helper methods until we figure which metadata should be where. - private static IBeatmapMetadataInfo getClosestMetadata(IBeatmapInfo beatmapInfo) => (beatmapInfo.Metadata ?? beatmapInfo.BeatmapSet.Metadata)!; + private static IBeatmapMetadataInfo getClosestMetadata(IBeatmapInfo beatmapInfo) => + beatmapInfo.Metadata ?? beatmapInfo.BeatmapSet?.Metadata ?? new BeatmapMetadata(); } } From e19be8ebe4eb49f456b61cd8f26c1143dad606ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 14:48:10 +0900 Subject: [PATCH 209/391] Make `GameplayState.Score` immutable --- osu.Game/Screens/Play/GameplayState.cs | 8 +++++--- osu.Game/Screens/Play/Player.cs | 20 ++++++++++---------- osu.Game/Screens/Play/ReplayPlayer.cs | 2 +- osu.Game/Screens/Play/SpectatorPlayer.cs | 3 ++- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayState.cs b/osu.Game/Screens/Play/GameplayState.cs index 9c83eddb45..44f72022f7 100644 --- a/osu.Game/Screens/Play/GameplayState.cs +++ b/osu.Game/Screens/Play/GameplayState.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.Framework.Bindables; using osu.Game.Beatmaps; @@ -36,7 +37,7 @@ namespace osu.Game.Screens.Play /// /// The gameplay score. /// - public Score? Score { get; set; } + public readonly Score Score; /// /// A bindable tracking the last judgement result applied to any hit object. @@ -45,11 +46,12 @@ namespace osu.Game.Screens.Play private readonly Bindable lastJudgementResult = new Bindable(); - public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList mods) + public GameplayState(IBeatmap beatmap, Ruleset ruleset, IReadOnlyList? mods = null, Score? score = null) { Beatmap = beatmap; Ruleset = ruleset; - Mods = mods; + Score = score ?? new Score(); + Mods = mods ?? ArraySegment.Empty; } /// diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 69093db883..a688330f54 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -161,14 +161,6 @@ namespace osu.Game.Screens.Play if (!LoadedBeatmapSuccessfully) return; - Score = CreateScore(); - GameplayState.Score = Score; - - // ensure the score is in a consistent state with the current player. - Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo; - Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; - Score.ScoreInfo.Mods = Mods.Value.ToArray(); - PrepareReplay(); ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(Score.ScoreInfo); @@ -226,7 +218,14 @@ namespace osu.Game.Screens.Play InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); - dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, Mods.Value)); + Score = CreateScore(playableBeatmap); + + // ensure the score is in a consistent state with the current player. + Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo; + Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; + Score.ScoreInfo.Mods = Mods.Value.ToArray(); + + dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, Mods.Value, Score)); AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); @@ -989,8 +988,9 @@ namespace osu.Game.Screens.Play /// /// Creates the player's . /// + /// /// The . - protected virtual Score CreateScore() => new Score + protected virtual Score CreateScore(IBeatmap beatmap) => new Score { ScoreInfo = new ScoreInfo { User = api.LocalUser.Value }, }; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index eefea737cf..93054b7bb5 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play DrawableRuleset?.SetReplayScore(Score); } - protected override Score CreateScore() => createScore(GameplayState.Beatmap, Mods.Value); + protected override Score CreateScore(IBeatmap beatmap) => createScore(beatmap, Mods.Value); // Don't re-import replay scores as they're already present in the database. protected override Task ImportScore(Score score) => Task.CompletedTask; diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index fbb4fb5699..f6a89e7fa9 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Spectator; @@ -79,7 +80,7 @@ namespace osu.Game.Screens.Play NonFrameStableSeek(score.Replay.Frames[0].Time); } - protected override Score CreateScore() => score; + protected override Score CreateScore(IBeatmap beatmap) => score; protected override ResultsScreen CreateResults(ScoreInfo score) => new SpectatorResultsScreen(score); From 7176dc95e5d9a100cd7486122031b8b7dcd2caea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 14:51:53 +0900 Subject: [PATCH 210/391] Revert `Player.Score` to `protected` --- 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 a688330f54..ecc65c6bb0 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -137,7 +137,7 @@ namespace osu.Game.Screens.Play public readonly PlayerConfiguration Configuration; - internal Score Score { get; private set; } + protected Score Score { get; private set; } /// /// Create a new player instance. From b6af93d43445b96db2adae8ebee954891b4b35d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 15:10:56 +0900 Subject: [PATCH 211/391] Apply some code quality refactoring --- .../Difficulty/DifficultyCalculator.cs | 19 +++++++++---------- .../Play/HUD/PerformancePointsCounter.cs | 19 +++++++------------ 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 3d90cc59f4..49a4b2c265 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -234,33 +234,32 @@ namespace osu.Game.Rulesets.Difficulty this.baseBeatmap = baseBeatmap; } + public readonly List HitObjects = new List(); + + IReadOnlyList IBeatmap.HitObjects => HitObjects; + + #region Delegated IBeatmap implementation + public BeatmapInfo BeatmapInfo { get => baseBeatmap.BeatmapInfo; set => baseBeatmap.BeatmapInfo = value; } - public BeatmapMetadata Metadata => baseBeatmap.Metadata; - public ControlPointInfo ControlPointInfo { get => baseBeatmap.ControlPointInfo; set => baseBeatmap.ControlPointInfo = value; } + public BeatmapMetadata Metadata => baseBeatmap.Metadata; public List Breaks => baseBeatmap.Breaks; - public double TotalBreakTime => baseBeatmap.TotalBreakTime; - - public readonly List HitObjects = new List(); - - IReadOnlyList IBeatmap.HitObjects => HitObjects; - public IEnumerable GetStatistics() => baseBeatmap.GetStatistics(); - public double GetMostCommonBeatLength() => baseBeatmap.GetMostCommonBeatLength(); - public IBeatmap Clone() => new ProgressiveCalculationBeatmap(baseBeatmap.Clone()); + + #endregion } } } diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 13b94e6cd6..7babc90427 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Threading; @@ -46,9 +45,6 @@ namespace osu.Game.Screens.Play.HUD [CanBeNull] private TimedDifficultyAttributes[] timedAttributes; - [CanBeNull] - private Ruleset gameplayRuleset; - private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource(); public PerformancePointsCounter() @@ -63,8 +59,8 @@ namespace osu.Game.Screens.Play.HUD if (gameplayState != null) { - gameplayRuleset = gameplayState.Ruleset; - difficultyCache.GetTimedDifficultyAttributesAsync(new GameplayWorkingBeatmap(gameplayState.Beatmap), gameplayRuleset, gameplayState.Mods.ToArray(), loadCancellationSource.Token) + var gameplayWorkingBeatmap = new GameplayWorkingBeatmap(gameplayState.Beatmap); + difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, gameplayState.Mods.ToArray(), loadCancellationSource.Token) .ContinueWith(r => Schedule(() => timedAttributes = r.Result), TaskContinuationOptions.OnlyOnRanToCompletion); } } @@ -82,15 +78,14 @@ namespace osu.Game.Screens.Play.HUD if (gameplayState?.Score == null || timedAttributes == null || timedAttributes.Length == 0) return; - Debug.Assert(gameplayRuleset != null); - - var attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); + int attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); if (attribIndex < 0) attribIndex = ~attribIndex - 1; attribIndex = Math.Clamp(attribIndex, 0, timedAttributes.Length - 1); - var ppProcessor = gameplayRuleset.CreatePerformanceCalculator(timedAttributes[attribIndex].Attributes, gameplayState.Score.ScoreInfo); - Current.Value = (int)Math.Round(ppProcessor?.Calculate() ?? 0, MidpointRounding.AwayFromZero); + var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(timedAttributes[attribIndex].Attributes, gameplayState.Score.ScoreInfo); + + Current.Value = (int)Math.Round(calculator?.Calculate() ?? 0, MidpointRounding.AwayFromZero); } protected override LocalisableString FormatCount(int count) => count.ToString(@"D"); @@ -145,7 +140,7 @@ namespace osu.Game.Screens.Play.HUD } } - // Todo: This class shouldn't exist, but requires breaking changes to allow DifficultyCalculator to receive an IBeatmap. + // TODO: This class shouldn't exist, but requires breaking changes to allow DifficultyCalculator to receive an IBeatmap. private class GameplayWorkingBeatmap : WorkingBeatmap { private readonly IBeatmap gameplayBeatmap; From 81a13566bc36574c943d9c4aa37fc7b47dcdd711 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 15:23:34 +0900 Subject: [PATCH 212/391] Adjust default location slightly, fix alignment of "pp" subtext --- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 3 ++- osu.Game/Skinning/DefaultSkin.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 7babc90427..2fc3f23190 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -133,7 +133,8 @@ namespace osu.Game.Screens.Play.HUD Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Text = @"pp", - Font = OsuFont.Numeric.With(size: 8) + Font = OsuFont.Numeric.With(size: 8), + Padding = new MarginPadding { Bottom = 1.5f }, // align baseline better } } }; diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 8c1e5313d5..8e03bddb4d 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -84,7 +84,7 @@ namespace osu.Game.Skinning if (ppCounter != null) { - ppCounter.Y = score.Position.Y + score.ScreenSpaceDrawQuad.Size.Y; + ppCounter.Y = score.Position.Y + ppCounter.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).Y - 4; ppCounter.Origin = Anchor.TopCentre; ppCounter.Anchor = Anchor.TopCentre; } From 676df55a0e40df9a48e3661cf4f6ed89a90828d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 15:39:29 +0900 Subject: [PATCH 213/391] Fade display out during rewind (as the value displayed is no longer valid) --- .../Graphics/UserInterface/RollingCounter.cs | 12 +++++---- .../Rulesets/Scoring/JudgementProcessor.cs | 7 +++++ .../Play/HUD/PerformancePointsCounter.cs | 26 +++++++++++++++++++ 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index 67b0b6a06b..16555075d1 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -25,7 +25,9 @@ namespace osu.Game.Graphics.UserInterface set => current.Current = value; } - private IHasText displayedCountSpriteText; + private IHasText displayedCountText; + + public Drawable DrawableCount { get; private set; } /// /// If true, the roll-up duration will be proportional to change in value. @@ -72,16 +74,16 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load() { - displayedCountSpriteText = CreateText(); + displayedCountText = CreateText(); UpdateDisplay(); - Child = (Drawable)displayedCountSpriteText; + Child = DrawableCount = (Drawable)displayedCountText; } protected void UpdateDisplay() { - if (displayedCountSpriteText != null) - displayedCountSpriteText.Text = FormatCount(DisplayedCount); + if (displayedCountText != null) + displayedCountText.Text = FormatCount(DisplayedCount); } protected override void LoadComplete() diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs index 201a05e569..ed4a16f0e8 100644 --- a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -18,6 +18,11 @@ namespace osu.Game.Rulesets.Scoring /// public event Action NewJudgement; + /// + /// Invoked when a judgement is reverted, usually due to rewinding gameplay. + /// + public event Action JudgementReverted; + /// /// The maximum number of hits that can be judged. /// @@ -71,6 +76,8 @@ namespace osu.Game.Rulesets.Scoring JudgedHits--; RevertResultInternal(result); + + JudgementReverted?.Invoke(result); } /// diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 2fc3f23190..b82a85691a 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -42,6 +42,9 @@ namespace osu.Game.Screens.Play.HUD [CanBeNull] private GameplayState gameplayState { get; set; } + [Resolved] + private GameplayClock gameplayClock { get; set; } + [CanBeNull] private TimedDifficultyAttributes[] timedAttributes; @@ -70,7 +73,24 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); if (scoreProcessor != null) + { scoreProcessor.NewJudgement += onNewJudgement; + scoreProcessor.JudgementReverted += onJudgementReverted; + } + } + + private bool isValid; + + protected bool IsValid + { + set + { + if (value == isValid) + return; + + isValid = value; + DrawableCount.FadeTo(isValid ? 1 : 0.3f, 1000, Easing.OutQuint); + } } private void onNewJudgement(JudgementResult judgement) @@ -86,6 +106,12 @@ namespace osu.Game.Screens.Play.HUD var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(timedAttributes[attribIndex].Attributes, gameplayState.Score.ScoreInfo); Current.Value = (int)Math.Round(calculator?.Calculate() ?? 0, MidpointRounding.AwayFromZero); + IsValid = true; + } + + private void onJudgementReverted(JudgementResult obj) + { + IsValid = false; } protected override LocalisableString FormatCount(int count) => count.ToString(@"D"); From 45b63cbad9dbb31a35cf2da3d3a88b2de8a5460a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 5 Oct 2021 16:03:25 +0900 Subject: [PATCH 214/391] Remove unnecessary dependency --- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index b82a85691a..c554a038ed 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -42,9 +42,6 @@ namespace osu.Game.Screens.Play.HUD [CanBeNull] private GameplayState gameplayState { get; set; } - [Resolved] - private GameplayClock gameplayClock { get; set; } - [CanBeNull] private TimedDifficultyAttributes[] timedAttributes; From 565e888f587e6bed57318d17eff71167e6820325 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 16:40:07 +0900 Subject: [PATCH 215/391] Tidy up attribute retrieval code --- .../Play/HUD/PerformancePointsCounter.cs | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index c554a038ed..fe34dd1d22 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -92,25 +92,32 @@ namespace osu.Game.Screens.Play.HUD private void onNewJudgement(JudgementResult judgement) { - if (gameplayState?.Score == null || timedAttributes == null || timedAttributes.Length == 0) + var attrib = getAttributeAtTime(judgement); + + if (gameplayState == null || attrib == null) return; - int attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); - if (attribIndex < 0) - attribIndex = ~attribIndex - 1; - attribIndex = Math.Clamp(attribIndex, 0, timedAttributes.Length - 1); - - var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(timedAttributes[attribIndex].Attributes, gameplayState.Score.ScoreInfo); + var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(attrib, gameplayState.Score.ScoreInfo); Current.Value = (int)Math.Round(calculator?.Calculate() ?? 0, MidpointRounding.AwayFromZero); IsValid = true; } - private void onJudgementReverted(JudgementResult obj) + [CanBeNull] + private DifficultyAttributes getAttributeAtTime(JudgementResult judgement) { - IsValid = false; + if (timedAttributes == null || timedAttributes.Length == 0) + return null; + + int attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); + if (attribIndex < 0) + attribIndex = ~attribIndex - 1; + + return timedAttributes[Math.Clamp(attribIndex, 0, timedAttributes.Length - 1)].Attributes; } + private void onJudgementReverted(JudgementResult obj) => IsValid = false; + protected override LocalisableString FormatCount(int count) => count.ToString(@"D"); protected override IHasText CreateText() => new TextComponent(); From eeb5f3d5191f87f20e9582caabab5792ab69bf2e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 16:48:54 +0900 Subject: [PATCH 216/391] Add basic test scene --- .../TestScenePerformancePointsCounter.cs | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs new file mode 100644 index 0000000000..350d08f63d --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.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.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; +using osuTK; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestScenePerformancePointsCounter : OsuTestScene + { + [Cached] + private GameplayState gameplayState; + + [Cached] + private ScoreProcessor scoreProcessor; + + private int iteration; + + public TestScenePerformancePointsCounter() + { + var ruleset = CreateRuleset(); + + Debug.Assert(ruleset != null); + + var beatmap = CreateWorkingBeatmap(ruleset.RulesetInfo) + .GetPlayableBeatmap(ruleset.RulesetInfo); + + gameplayState = new GameplayState(beatmap, ruleset); + scoreProcessor = new ScoreProcessor(); + } + + protected override Ruleset CreateRuleset() => new OsuRuleset(); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Create counter", () => + { + Child = new PerformancePointsCounter + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(5), + }; + }); + + AddRepeatStep("Add judgement", () => + { + var scoreInfo = gameplayState.Score.ScoreInfo; + + scoreInfo.MaxCombo = iteration * 1000; + scoreInfo.Accuracy = 1; + scoreInfo.Statistics[HitResult.Great] = iteration * 1000; + + scoreProcessor.ApplyResult(new OsuJudgementResult(new HitObject + { + StartTime = iteration * 10000, + }, new OsuJudgement()) + { + Type = HitResult.Perfect, + }); + + iteration++; + }, 10); + + AddStep("Revert judgement", () => + { + scoreProcessor.RevertResult(new JudgementResult(new HitObject(), new OsuJudgement())); + }); + } + } +} From fa7f11d906f8214dc15b01b04b405f8f957909ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 16:51:49 +0900 Subject: [PATCH 217/391] Add easing to rolling counter value --- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index fe34dd1d22..ab10399903 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -34,6 +34,10 @@ namespace osu.Game.Screens.Play.HUD { public bool UsesFixedAnchor { get; set; } + protected override bool IsRollingProportional => true; + + protected override double RollingDuration => 1000; + [CanBeNull] [Resolved(CanBeNull = true)] private ScoreProcessor scoreProcessor { get; set; } From 599d82e383f167632719758554b024fb4ab58bf4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 16:59:54 +0900 Subject: [PATCH 218/391] Avoid returning a live `IEnumerable` --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 4 ++-- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 10 +++++++--- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 8 ++++---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index e6c287112f..3777365088 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -148,9 +148,9 @@ namespace osu.Game.Beatmaps }, token, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); } - public Task GetTimedDifficultyAttributesAsync(WorkingBeatmap beatmap, Ruleset ruleset, Mod[] mods, CancellationToken token = default) + public Task> GetTimedDifficultyAttributesAsync(WorkingBeatmap beatmap, Ruleset ruleset, Mod[] mods, CancellationToken token = default) { - return Task.Factory.StartNew(() => ruleset.CreateDifficultyCalculator(beatmap).CalculateTimed(mods).ToArray(), + return Task.Factory.StartNew(() => ruleset.CreateDifficultyCalculator(beatmap).CalculateTimed(mods), token, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 49a4b2c265..a7c4790366 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -58,12 +58,14 @@ namespace osu.Game.Rulesets.Difficulty return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate); } - public IEnumerable CalculateTimed(params Mod[] mods) + public List CalculateTimed(params Mod[] mods) { preProcess(mods); + var attribs = new List(); + if (!Beatmap.HitObjects.Any()) - yield break; + return attribs; var skills = CreateSkills(Beatmap, playableMods, clockRate); var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap); @@ -75,8 +77,10 @@ namespace osu.Game.Rulesets.Difficulty foreach (var skill in skills) skill.ProcessInternal(hitObject); - yield return new TimedDifficultyAttributes(hitObject.EndTime, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate)); + attribs.Add(new TimedDifficultyAttributes(hitObject.EndTime, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate))); } + + return attribs; } /// diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index ab10399903..514369735e 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Play.HUD private GameplayState gameplayState { get; set; } [CanBeNull] - private TimedDifficultyAttributes[] timedAttributes; + private List timedAttributes; private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource(); @@ -110,14 +110,14 @@ namespace osu.Game.Screens.Play.HUD [CanBeNull] private DifficultyAttributes getAttributeAtTime(JudgementResult judgement) { - if (timedAttributes == null || timedAttributes.Length == 0) + if (timedAttributes == null || timedAttributes.Count == 0) return null; - int attribIndex = Array.BinarySearch(timedAttributes, 0, timedAttributes.Length, new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); + int attribIndex = timedAttributes.BinarySearch(new TimedDifficultyAttributes(judgement.HitObject.GetEndTime(), null)); if (attribIndex < 0) attribIndex = ~attribIndex - 1; - return timedAttributes[Math.Clamp(attribIndex, 0, timedAttributes.Length - 1)].Attributes; + return timedAttributes[Math.Clamp(attribIndex, 0, timedAttributes.Count - 1)].Attributes; } private void onJudgementReverted(JudgementResult obj) => IsValid = false; From 04538a69e403fcbd8bbcf3e8e2baea5afc00191e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 17:10:24 +0900 Subject: [PATCH 219/391] Add assert tests --- .../TestScenePerformancePointsCounter.cs | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs index 350d08f63d..26f4bda171 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Diagnostics; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; @@ -26,6 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay private ScoreProcessor scoreProcessor; private int iteration; + private PerformancePointsCounter counter; public TestScenePerformancePointsCounter() { @@ -47,37 +49,55 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("Create counter", () => { - Child = new PerformancePointsCounter + iteration = 0; + + Child = counter = new PerformancePointsCounter { Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(5), }; }); + } - AddRepeatStep("Add judgement", () => - { - var scoreInfo = gameplayState.Score.ScoreInfo; + [Test] + public void TestBasicCounting() + { + AddAssert("counter displaying zero", () => counter.Current.Value == 0); - scoreInfo.MaxCombo = iteration * 1000; - scoreInfo.Accuracy = 1; - scoreInfo.Statistics[HitResult.Great] = iteration * 1000; + AddRepeatStep("Add judgement", applyOneJudgement, 10); - scoreProcessor.ApplyResult(new OsuJudgementResult(new HitObject - { - StartTime = iteration * 10000, - }, new OsuJudgement()) - { - Type = HitResult.Perfect, - }); - - iteration++; - }, 10); + AddUntilStep("counter non-zero", () => counter.Current.Value > 0); AddStep("Revert judgement", () => { scoreProcessor.RevertResult(new JudgementResult(new HitObject(), new OsuJudgement())); }); + + AddUntilStep("counter faded", () => counter.Child.Alpha < 1); + + AddStep("Add judgement", applyOneJudgement); + + AddUntilStep("counter opaque", () => counter.Child.Alpha == 1); + } + + private void applyOneJudgement() + { + var scoreInfo = gameplayState.Score.ScoreInfo; + + scoreInfo.MaxCombo = iteration * 1000; + scoreInfo.Accuracy = 1; + scoreInfo.Statistics[HitResult.Great] = iteration * 1000; + + scoreProcessor.ApplyResult(new OsuJudgementResult(new HitObject + { + StartTime = iteration * 10000, + }, new OsuJudgement()) + { + Type = HitResult.Perfect, + }); + + iteration++; } } } From f64226ded6da72516c59a65047b19b1eaf4427b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 17:10:32 +0900 Subject: [PATCH 220/391] Fix display not displaying correctly after initial load --- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 514369735e..da821d76ad 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -51,6 +51,8 @@ namespace osu.Game.Screens.Play.HUD private readonly CancellationTokenSource loadCancellationSource = new CancellationTokenSource(); + private JudgementResult lastJudgement; + public PerformancePointsCounter() { Current.Value = DisplayedCount = 0; @@ -65,7 +67,12 @@ namespace osu.Game.Screens.Play.HUD { var gameplayWorkingBeatmap = new GameplayWorkingBeatmap(gameplayState.Beatmap); difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, gameplayState.Mods.ToArray(), loadCancellationSource.Token) - .ContinueWith(r => Schedule(() => timedAttributes = r.Result), TaskContinuationOptions.OnlyOnRanToCompletion); + .ContinueWith(r => Schedule(() => + { + timedAttributes = r.Result; + if (lastJudgement != null) + onNewJudgement(lastJudgement); + }), TaskContinuationOptions.OnlyOnRanToCompletion); } } @@ -96,6 +103,8 @@ namespace osu.Game.Screens.Play.HUD private void onNewJudgement(JudgementResult judgement) { + lastJudgement = judgement; + var attrib = getAttributeAtTime(judgement); if (gameplayState == null || attrib == null) From 1e4da8112088b5a6a315fa4b962e3e7f5245c39a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 17:14:09 +0900 Subject: [PATCH 221/391] Fix import notifications not showing correct text --- osu.Game/Database/ArchiveModelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 9ad2dec12e..ee1a7e2900 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -197,7 +197,7 @@ namespace osu.Game.Database else { notification.CompletionText = imported.Count == 1 - ? $"Imported {imported.First()}!" + ? $"Imported {imported.First().Value}!" : $"Imported {imported.Count} {HumanisedModelName}s!"; if (imported.Count > 0 && PostImport != null) From 0859c336de52dcb5d69492f02ef5de704d782653 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 17:24:36 +0900 Subject: [PATCH 222/391] Also dim counter during initial calculation phase --- .../Gameplay/TestScenePerformancePointsCounter.cs | 1 + osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 10 ++++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs index 26f4bda171..c7d2204de2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs @@ -68,6 +68,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddRepeatStep("Add judgement", applyOneJudgement, 10); AddUntilStep("counter non-zero", () => counter.Current.Value > 0); + AddUntilStep("counter opaque", () => counter.Child.Alpha == 1); AddStep("Revert judgement", () => { diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index da821d76ad..9eac62fe73 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -38,6 +38,8 @@ namespace osu.Game.Screens.Play.HUD protected override double RollingDuration => 1000; + private const float alpha_when_invalid = 0.3f; + [CanBeNull] [Resolved(CanBeNull = true)] private ScoreProcessor scoreProcessor { get; set; } @@ -70,6 +72,7 @@ namespace osu.Game.Screens.Play.HUD .ContinueWith(r => Schedule(() => { timedAttributes = r.Result; + IsValid = true; if (lastJudgement != null) onNewJudgement(lastJudgement); }), TaskContinuationOptions.OnlyOnRanToCompletion); @@ -97,7 +100,7 @@ namespace osu.Game.Screens.Play.HUD return; isValid = value; - DrawableCount.FadeTo(isValid ? 1 : 0.3f, 1000, Easing.OutQuint); + DrawableCount.FadeTo(isValid ? 1 : alpha_when_invalid, 1000, Easing.OutQuint); } } @@ -133,7 +136,10 @@ namespace osu.Game.Screens.Play.HUD protected override LocalisableString FormatCount(int count) => count.ToString(@"D"); - protected override IHasText CreateText() => new TextComponent(); + protected override IHasText CreateText() => new TextComponent + { + Alpha = alpha_when_invalid + }; protected override void Dispose(bool isDisposing) { From 2be44188efbd6befbd4a39242a9f823509d7e35c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 17:59:38 +0900 Subject: [PATCH 223/391] Add missing null checks --- osu.Game/Overlays/RankingsOverlay.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index 2263d54d7b..80ce2e038d 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -143,6 +143,9 @@ namespace osu.Game.Overlays switch (request) { case GetUserRankingsRequest userRequest: + if (userRequest.Response == null) + return null; + switch (userRequest.Type) { case UserRankingsType.Performance: @@ -155,7 +158,12 @@ namespace osu.Game.Overlays return null; case GetCountryRankingsRequest countryRequest: + { + if (countryRequest.Response == null) + return null; + return new CountriesTable(1, countryRequest.Response.Countries); + } } return null; From 94153e8bba3293d7141ab4091d0ab3203c20280f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 18:06:24 +0900 Subject: [PATCH 224/391] Fix `TestDifficultyIconSelectingForDifferentRuleset` potentially failing due to async load --- osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 70224ae9f2..067f1cabb4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -714,10 +714,11 @@ namespace osu.Game.Tests.Visual.SongSelect }); FilterableDifficultyIcon difficultyIcon = null; - AddStep("Find an icon for different ruleset", () => + AddUntilStep("Find an icon for different ruleset", () => { difficultyIcon = set.ChildrenOfType() - .First(icon => icon.Item.BeatmapInfo.Ruleset.ID == 3); + .FirstOrDefault(icon => icon.Item.BeatmapInfo.Ruleset.ID == 3); + return difficultyIcon != null; }); AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); From 5d708b612d3c4326c56a308b5e42723d00d91ed0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 18:17:20 +0900 Subject: [PATCH 225/391] Fix delete local score test not waiting for "fetch" to complete Even though this is a completely local operation in this case, there's still a level of asynchronous operation which was recent introduced with the score ordering: https://github.com/ppy/osu/blob/853cf6feaa165e833ecb7ca18c6cdffe8ca6e005/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs#L159 This means there is a brief period where the `Scores` property is null, after `Reset()` is called in the re-fetch procedure. --- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index d0a76bac27..189b143a35 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -162,6 +162,8 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.Click(MouseButton.Left); }); + AddUntilStep("wait for fetch", () => leaderboard.Scores != null); + AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID)); } From e6efdae7c95967543caedd282f91cf03b2ea953d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 18:53:20 +0900 Subject: [PATCH 226/391] Add various logging output in an atttempt to figure multiplayer test failure --- osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs | 3 +++ osu.Game/Tests/Visual/ScreenTestScene.cs | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index a8fda19c60..c040ab27e4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.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.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -68,6 +69,8 @@ namespace osu.Game.Tests.Visual.Multiplayer LoadScreen(dependenciesScreen = new DependenciesScreen(client)); }); + AddUntilStep("wait for dependencies screen", () => Stack.CurrentScreen is DependenciesScreen); + AddUntilStep("wait for dependencies to start load", () => dependenciesScreen.LoadState > LoadState.Ready); AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded); AddStep("load multiplayer", () => LoadScreen(multiplayerScreen)); diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index b30be05ac4..966c513269 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -4,6 +4,8 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; +using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Overlays; using osu.Game.Screens; @@ -32,6 +34,9 @@ namespace osu.Game.Tests.Visual content = new Container { RelativeSizeAxes = Axes.Both }, DialogOverlay = new DialogOverlay() }); + + Stack.ScreenPushed += (lastScreen, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed → {newScreen}"); + Stack.ScreenExited += (lastScreen, newScreen) => Logger.Log($"{nameof(ScreenTestScene)} screen changed ← {newScreen}"); } protected void LoadScreen(OsuScreen screen) => Stack.Push(screen); From 98fef6ece2d39efb59ea89384a7799b5f7c0efba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Oct 2021 19:08:30 +0900 Subject: [PATCH 227/391] Handle judgement reverts with actual display updates --- .../Gameplay/TestScenePerformancePointsCounter.cs | 8 ++++++-- .../Screens/Play/HUD/PerformancePointsCounter.cs | 15 ++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs index c7d2204de2..4c48d52acd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePerformancePointsCounter.cs @@ -63,6 +63,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestBasicCounting() { + int previousValue = 0; + AddAssert("counter displaying zero", () => counter.Current.Value == 0); AddRepeatStep("Add judgement", applyOneJudgement, 10); @@ -72,14 +74,16 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Revert judgement", () => { + previousValue = counter.Current.Value; + scoreProcessor.RevertResult(new JudgementResult(new HitObject(), new OsuJudgement())); }); - AddUntilStep("counter faded", () => counter.Child.Alpha < 1); + AddUntilStep("counter decreased", () => counter.Current.Value < previousValue); AddStep("Add judgement", applyOneJudgement); - AddUntilStep("counter opaque", () => counter.Child.Alpha == 1); + AddUntilStep("counter non-zero", () => counter.Current.Value > 0); } private void applyOneJudgement() diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 9eac62fe73..2ae7b5660a 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -74,7 +74,7 @@ namespace osu.Game.Screens.Play.HUD timedAttributes = r.Result; IsValid = true; if (lastJudgement != null) - onNewJudgement(lastJudgement); + onJudgementChanged(lastJudgement); }), TaskContinuationOptions.OnlyOnRanToCompletion); } } @@ -85,8 +85,8 @@ namespace osu.Game.Screens.Play.HUD if (scoreProcessor != null) { - scoreProcessor.NewJudgement += onNewJudgement; - scoreProcessor.JudgementReverted += onJudgementReverted; + scoreProcessor.NewJudgement += onJudgementChanged; + scoreProcessor.JudgementReverted += onJudgementChanged; } } @@ -104,14 +104,17 @@ namespace osu.Game.Screens.Play.HUD } } - private void onNewJudgement(JudgementResult judgement) + private void onJudgementChanged(JudgementResult judgement) { lastJudgement = judgement; var attrib = getAttributeAtTime(judgement); if (gameplayState == null || attrib == null) + { + IsValid = false; return; + } var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(attrib, gameplayState.Score.ScoreInfo); @@ -132,8 +135,6 @@ namespace osu.Game.Screens.Play.HUD return timedAttributes[Math.Clamp(attribIndex, 0, timedAttributes.Count - 1)].Attributes; } - private void onJudgementReverted(JudgementResult obj) => IsValid = false; - protected override LocalisableString FormatCount(int count) => count.ToString(@"D"); protected override IHasText CreateText() => new TextComponent @@ -146,7 +147,7 @@ namespace osu.Game.Screens.Play.HUD base.Dispose(isDisposing); if (scoreProcessor != null) - scoreProcessor.NewJudgement -= onNewJudgement; + scoreProcessor.NewJudgement -= onJudgementChanged; loadCancellationSource?.Cancel(); } From 12da27cde728b0d17517e23306b0e180be9e75d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 5 Oct 2021 20:52:40 +0200 Subject: [PATCH 228/391] Add test coverage for loading process on channel join --- .../Visual/Online/TestSceneChatOverlay.cs | 38 ++++++++++++++++++- .../Online/API/Requests/GetMessagesRequest.cs | 6 +-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 609e637914..9562b41363 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -18,6 +19,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Chat; using osu.Game.Overlays; +using osu.Game.Overlays.Chat; using osu.Game.Overlays.Chat.Selection; using osu.Game.Overlays.Chat.Tabs; using osu.Game.Users; @@ -41,6 +43,9 @@ namespace osu.Game.Tests.Visual.Online private Channel channel2 => channels[1]; private Channel channel3 => channels[2]; + [CanBeNull] + private Func> onGetMessages; + [Resolved] private GameHost host { get; set; } @@ -79,6 +84,8 @@ namespace osu.Game.Tests.Visual.Online { AddStep("register request handling", () => { + onGetMessages = null; + ((DummyAPIAccess)API).HandleRequest = req => { switch (req) @@ -102,6 +109,12 @@ namespace osu.Game.Tests.Visual.Online } return true; + + case GetMessagesRequest getMessages: + var messages = onGetMessages?.Invoke(getMessages.Channel); + if (messages != null) + getMessages.TriggerSuccess(messages); + return true; } return false; @@ -122,14 +135,37 @@ namespace osu.Game.Tests.Visual.Online } [Test] - public void TestSelectingChannelClosesSelector() + public void TestChannelSelection() { AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible); + AddStep("Setup get message response", () => onGetMessages = channel => + { + if (channel == channel1) + { + return new List + { + new Message(1) + { + ChannelId = channel1.Id, + Content = "hello from channel 1!", + Sender = new User + { + Id = 2, + Username = "test_user" + } + } + }; + } + + return null; + }); AddStep("Join channel 1", () => channelManager.JoinChannel(channel1)); AddStep("Switch to channel 1", () => clickDrawable(chatOverlay.TabMap[channel1])); AddAssert("Current channel is channel 1", () => currentChannel == channel1); + AddUntilStep("Loading spinner hidden", () => chatOverlay.ChildrenOfType().All(spinner => !spinner.IsPresent)); + AddAssert("Channel message shown", () => chatOverlay.ChildrenOfType().Count() == 1); AddAssert("Channel selector was closed", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); } diff --git a/osu.Game/Online/API/Requests/GetMessagesRequest.cs b/osu.Game/Online/API/Requests/GetMessagesRequest.cs index 36e81a9348..651f8a06c5 100644 --- a/osu.Game/Online/API/Requests/GetMessagesRequest.cs +++ b/osu.Game/Online/API/Requests/GetMessagesRequest.cs @@ -8,13 +8,13 @@ namespace osu.Game.Online.API.Requests { public class GetMessagesRequest : APIRequest> { - private readonly Channel channel; + public readonly Channel Channel; public GetMessagesRequest(Channel channel) { - this.channel = channel; + Channel = channel; } - protected override string Target => $@"chat/channels/{channel.Id}/messages"; + protected override string Target => $@"chat/channels/{Channel.Id}/messages"; } } From a5b07ce4feaf88cf2f2f5eee6e43b166a8207737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 5 Oct 2021 20:53:06 +0200 Subject: [PATCH 229/391] Fix backwards containment check in chat channel load callback --- osu.Game/Overlays/ChatOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 20d637d957..4b27335c7c 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -285,7 +285,7 @@ namespace osu.Game.Overlays return; // check once more to ensure the channel hasn't since been removed from the loaded channels list (may have been left by some automated means). - if (loadedChannels.Contains(loaded)) + if (!loadedChannels.Contains(loaded)) return; loading.Hide(); From baa8baaa1efd762b650c4e2b9e409f700e601fe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 5 Oct 2021 21:12:35 +0200 Subject: [PATCH 230/391] Fix "most played beatmap" request breakage after property rename --- .../Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs index 15f67eda47..10f7ca6fe2 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUserMostPlayedBeatmap.cs @@ -15,7 +15,7 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("count")] public int PlayCount { get; set; } - [JsonProperty] + [JsonProperty("beatmap")] private BeatmapInfo beatmapInfo { get; set; } [JsonProperty] From 3cb816b6cd2165788a288d51457ec737c52944e2 Mon Sep 17 00:00:00 2001 From: emu1337 Date: Tue, 5 Oct 2021 21:19:00 +0200 Subject: [PATCH 231/391] fixed some nerfs not carrying to the next iteration --- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 4048d84d35..c981348e44 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2; - startRatio = windowPenalty * currRatio; + startRatio = effectiveRatio; previousIslandSize = islandSize; // log the last island size. From 777763a55095147c307b32e502aa5fd9220f5066 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 05:27:36 +0900 Subject: [PATCH 232/391] Add more comprehensive (and failing) test coverage of replay download button --- .../Gameplay/TestSceneReplayDownloadButton.cs | 84 +++++++++++++++++-- 1 file changed, 75 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index 1809332bce..5dd4ecf30b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -8,34 +8,97 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Scoring; using osu.Game.Users; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Game.Rulesets; using osu.Game.Screens.Ranking; +using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { [TestFixture] - public class TestSceneReplayDownloadButton : OsuTestScene + public class TestSceneReplayDownloadButton : OsuManualInputManagerTestScene { [Resolved] private RulesetStore rulesets { get; set; } private TestReplayDownloadButton downloadButton; - public TestSceneReplayDownloadButton() + [Test] + public void TestDisplayStates() { - createButton(true); + AddStep(@"create button with replay", () => + { + Child = downloadButton = new TestReplayDownloadButton(getScoreInfo(true)) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + }); + + AddUntilStep("wait for load", () => downloadButton.IsLoaded); + + AddStep("click button", () => downloadButton.TriggerClick()); + AddStep(@"downloading state", () => downloadButton.SetDownloadState(DownloadState.Downloading)); AddStep(@"locally available state", () => downloadButton.SetDownloadState(DownloadState.LocallyAvailable)); AddStep(@"not downloaded state", () => downloadButton.SetDownloadState(DownloadState.NotDownloaded)); - createButton(false); - createButtonNoScore(); } - private void createButton(bool withReplay) + [Test] + public void TestButtonWithReplayStartsDownload() { - AddStep(withReplay ? @"create button with replay" : "create button without replay", () => + bool downloadStarted = false; + bool downloadFinished = false; + + AddStep(@"create button with replay", () => { - Child = downloadButton = new TestReplayDownloadButton(getScoreInfo(withReplay)) + downloadStarted = false; + downloadFinished = false; + + Child = downloadButton = new TestReplayDownloadButton(getScoreInfo(true)) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + + downloadButton.State.BindValueChanged(state => + { + switch (state.NewValue) + { + case DownloadState.Downloading: + downloadStarted = true; + break; + } + + switch (state.OldValue) + { + case DownloadState.Downloading: + downloadFinished = true; + break; + } + }); + }); + + AddUntilStep("wait for load", () => downloadButton.IsLoaded); + + AddAssert("state is available", () => downloadButton.State.Value == DownloadState.NotDownloaded); + + AddStep("click button", () => + { + InputManager.MoveMouseTo(downloadButton); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("state entered downloading", () => downloadStarted); + AddUntilStep("state left downloading", () => downloadFinished); + } + + [Test] + public void TestButtonWithoutReplay() + { + AddStep("create button without replay", () => + { + Child = downloadButton = new TestReplayDownloadButton(getScoreInfo(false)) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -45,7 +108,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for load", () => downloadButton.IsLoaded); } - private void createButtonNoScore() + [Test] + public void CreateButtonWithNoScore() { AddStep("create button with null score", () => { @@ -78,6 +142,8 @@ namespace osu.Game.Tests.Visual.Gameplay { public void SetDownloadState(DownloadState state) => State.Value = state; + public new Bindable State => base.State; + public TestReplayDownloadButton(ScoreInfo score) : base(score) { From 5a4474e1b2560896c3f09dc2d4fc94c437b70d88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 05:28:11 +0900 Subject: [PATCH 233/391] Fix incorrect DI retrieval in `ReplayDownloadButton` --- osu.Game/Screens/Ranking/ReplayDownloadButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index d96b6989b4..e644eb671a 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Ranking } [BackgroundDependencyLoader(true)] - private void load(OsuGame game, ScoreModelDownloader scores) + private void load(OsuGame game, ScoreManager scores) { InternalChild = shakeContainer = new ShakeContainer { @@ -60,7 +60,7 @@ namespace osu.Game.Screens.Ranking break; case DownloadState.NotDownloaded: - scores.Download(Model.Value); + scores.Download(Model.Value, false); break; case DownloadState.Importing: From 1f6a31355c514e809ca61a64f3a82e3a608e1777 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 05:30:49 +0900 Subject: [PATCH 234/391] Remove unused using statement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Tests/Visual/ScreenTestScene.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Tests/Visual/ScreenTestScene.cs b/osu.Game/Tests/Visual/ScreenTestScene.cs index 966c513269..aa46b516bf 100644 --- a/osu.Game/Tests/Visual/ScreenTestScene.cs +++ b/osu.Game/Tests/Visual/ScreenTestScene.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Logging; -using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Overlays; using osu.Game.Screens; From 1a784b788dba00e79be4011b2af652d9250d3be4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 05:31:07 +0900 Subject: [PATCH 235/391] Fix incorrect load state check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs index c040ab27e4..80217a7726 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); AddUntilStep("wait for dependencies screen", () => Stack.CurrentScreen is DependenciesScreen); - AddUntilStep("wait for dependencies to start load", () => dependenciesScreen.LoadState > LoadState.Ready); + AddUntilStep("wait for dependencies to start load", () => dependenciesScreen.LoadState > LoadState.NotLoaded); AddUntilStep("wait for dependencies to load", () => dependenciesScreen.IsLoaded); AddStep("load multiplayer", () => LoadScreen(multiplayerScreen)); From 31c0c7a8881e56a02b44794a3d2406548e79b69a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 05:49:04 +0900 Subject: [PATCH 236/391] Remove pointless (and incorrect) click step --- osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index 5dd4ecf30b..8197e09594 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -37,8 +37,6 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for load", () => downloadButton.IsLoaded); - AddStep("click button", () => downloadButton.TriggerClick()); - AddStep(@"downloading state", () => downloadButton.SetDownloadState(DownloadState.Downloading)); AddStep(@"locally available state", () => downloadButton.SetDownloadState(DownloadState.LocallyAvailable)); AddStep(@"not downloaded state", () => downloadButton.SetDownloadState(DownloadState.NotDownloaded)); From d6f25e07ccc453bc06de28fef5eefaa2540e1e4f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 05:49:18 +0900 Subject: [PATCH 237/391] Add assert coverage of non-downloadable states --- .../Visual/Gameplay/TestSceneReplayDownloadButton.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index 8197e09594..5e2374cbcb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.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.Game.Online; @@ -9,6 +10,8 @@ using osu.Game.Scoring; using osu.Game.Users; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Testing; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osu.Game.Screens.Ranking; using osuTK.Input; @@ -104,6 +107,9 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddUntilStep("wait for load", () => downloadButton.IsLoaded); + + AddAssert("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded); + AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value); } [Test] @@ -119,6 +125,9 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddUntilStep("wait for load", () => downloadButton.IsLoaded); + + AddAssert("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded); + AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType().First().Enabled.Value); } private ScoreInfo getScoreInfo(bool replayAvailable) From 4d5696959b0435bf4bcb0fad96b7c233eaf2bdb6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 05:52:28 +0900 Subject: [PATCH 238/391] Remove unnecessary access modifier in interface Co-authored-by: Dan Balasescu --- osu.Game/Rulesets/IRulesetInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/IRulesetInfo.cs b/osu.Game/Rulesets/IRulesetInfo.cs index ded3ac4b58..779433dc81 100644 --- a/osu.Game/Rulesets/IRulesetInfo.cs +++ b/osu.Game/Rulesets/IRulesetInfo.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets /// string InstantiationInfo { get; } - public Ruleset? CreateInstance() + Ruleset? CreateInstance() { var type = Type.GetType(InstantiationInfo); From 4f59fc15a50910eb4716f3968779e64b28856acc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 05:54:37 +0900 Subject: [PATCH 239/391] Mark `BeatmapSet` as nullable for the time being --- osu.Game/Beatmaps/IBeatmapInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/IBeatmapInfo.cs b/osu.Game/Beatmaps/IBeatmapInfo.cs index 6a3f1b43d8..3d51c5d4b6 100644 --- a/osu.Game/Beatmaps/IBeatmapInfo.cs +++ b/osu.Game/Beatmaps/IBeatmapInfo.cs @@ -31,7 +31,7 @@ namespace osu.Game.Beatmaps /// /// The beatmap set this beatmap is part of. /// - IBeatmapSetInfo BeatmapSet { get; } + IBeatmapSetInfo? BeatmapSet { get; } /// /// The playable length in milliseconds of this beatmap. From d17beb9bbe0ab1e0551ea210c01515757aa073b8 Mon Sep 17 00:00:00 2001 From: emu1337 Date: Wed, 6 Oct 2021 01:39:01 +0200 Subject: [PATCH 240/391] improved overall balance --- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index c981348e44..cae6b8e01c 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 const double single_spacing_threshold = 125; - private const double rhythm_multiplier = 0.675; + private const double rhythm_multiplier = 0.75; private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max. private const double min_speed_bonus = 75; // ~200BPM private const double speed_balancing_factor = 40; @@ -70,11 +70,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills double currDelta = currObj.StrainTime; double prevDelta = prevObj.StrainTime; double lastDelta = lastObj.StrainTime; - double currRatio = 1.0 + Math.Min(4.5, 6 * 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.Max(prevDelta, currDelta) - Math.Min(prevDelta, currDelta) - greatWindow) / greatWindow); + double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6)); - windowPenalty = Math.Min(1, windowPenalty * (previousIslandSize + islandSize)); + windowPenalty = Math.Min(1, windowPenalty); + + double effectiveRatio = windowPenalty * currRatio; if (firstDeltaSwitch) { @@ -85,8 +87,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills } else { - double effectiveRatio = windowPenalty * currRatio; - if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window effectiveRatio *= 0.125; @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills { // Begin counting island until we change speed again. firstDeltaSwitch = true; - startRatio = windowPenalty * currRatio; + startRatio = effectiveRatio; islandSize = 1; } } From df182ba92ba94030c136a816ecc2c2cf8fb80e12 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 6 Oct 2021 11:27:13 +0900 Subject: [PATCH 241/391] Set fQ to recommended value from BASS developer to prevent filter calculations from overflowing when approaching nyquist --- osu.Game/Audio/Effects/Filter.cs | 43 ++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/osu.Game/Audio/Effects/Filter.cs b/osu.Game/Audio/Effects/Filter.cs index 142e6b8fff..00b617ffa1 100644 --- a/osu.Game/Audio/Effects/Filter.cs +++ b/osu.Game/Audio/Effects/Filter.cs @@ -10,7 +10,7 @@ namespace osu.Game.Audio.Effects { public class Filter : Component, ITransformableFilter { - public readonly int MaxCutoff; + public readonly int MaxCutoff = 22049; // nyquist - 1hz private readonly AudioMixer mixer; private readonly BQFParameters filter; private readonly BQFType type; @@ -18,36 +18,29 @@ namespace osu.Game.Audio.Effects public BindableNumber Cutoff { get; } /// - /// A BiQuad filter that performs a filter-sweep when toggled on or off. + /// A Component that implements a BASS FX BiQuad Filter Effect. /// - /// The mixer this effect should be attached to. + /// The mixer this effect should be applied to. /// The type of filter (e.g. LowPass, HighPass, etc) public Filter(AudioMixer mixer, BQFType type = BQFType.LowPass) { this.mixer = mixer; this.type = type; - var initialCutoff = 1; + int initialCutoff; - // These max cutoff values are a work-around for BASS' BiQuad filters behaving weirdly when approaching nyquist. - // Note that these values assume a sample rate of 44100 (as per BassAudioMixer in osu.Framework) - // See also https://www.un4seen.com/forum/?topic=19542.0 for more information. switch (type) { case BQFType.HighPass: - MaxCutoff = 21968; // beyond this value, the high-pass cuts out + initialCutoff = 1; break; case BQFType.LowPass: - MaxCutoff = initialCutoff = 14000; // beyond (roughly) this value, the low-pass filter audibly wraps/reflects - break; - - case BQFType.BandPass: - MaxCutoff = 16000; // beyond (roughly) this value, the band-pass filter audibly wraps/reflects + initialCutoff = MaxCutoff; break; default: - MaxCutoff = 22050; // default to nyquist for other filter types, TODO: handle quirks of other filter types + initialCutoff = 500; // A default that should ensure audio remains audible for other filters. break; } @@ -59,11 +52,12 @@ namespace osu.Game.Audio.Effects filter = new BQFParameters { lFilter = type, - fCenter = initialCutoff + fCenter = initialCutoff, + fBandwidth = 0, + fQ = 0.7f // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0) }; attachFilter(); - Cutoff.ValueChanged += updateFilter; Cutoff.Value = initialCutoff; } @@ -74,9 +68,7 @@ namespace osu.Game.Audio.Effects private void updateFilter(ValueChangedEvent cutoff) { - // This is another workaround for quirks in BASS' BiQuad filters. - // Because the cutoff can't be set above ~14khz (i.e. outside of human hearing range) without the aforementioned wrapping/reflecting quirk occuring, we instead - // remove the effect from the mixer when the cutoff is at maximum so that a LowPass filter isn't always attenuating high frequencies just by existing. + // Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz. if (type == BQFType.LowPass) { if (cutoff.NewValue >= MaxCutoff) @@ -89,6 +81,19 @@ namespace osu.Game.Audio.Effects attachFilter(); } + // Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz. + if (type == BQFType.HighPass) + { + if (cutoff.NewValue <= 1) + { + detachFilter(); + return; + } + + if (cutoff.OldValue <= 1 && cutoff.NewValue > 1) + attachFilter(); + } + var filterIndex = mixer.Effects.IndexOf(filter); if (filterIndex < 0) return; From 266e62794e4c1f892f05e4ddbdd2b8a0b3eba289 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 6 Oct 2021 11:50:14 +0900 Subject: [PATCH 242/391] Tweak cutoff effect for new Q value --- osu.Game/Overlays/DialogOverlay.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 6016d16d29..bd20b74970 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -82,13 +82,15 @@ namespace osu.Game.Overlays { base.PopIn(); this.FadeIn(PopupDialog.ENTER_DURATION, Easing.OutQuint); - lpFilter.CutoffTo(2000).Then().CutoffTo(150, 100, Easing.OutCubic); + lpFilter.CutoffTo(300, 100, Easing.OutCubic); } protected override void PopOut() { base.PopOut(); + lpFilter.CutoffTo(lpFilter.MaxCutoff, 100, Easing.InCubic); + if (CurrentDialog?.State.Value == Visibility.Visible) { CurrentDialog.Hide(); @@ -96,7 +98,6 @@ namespace osu.Game.Overlays } this.FadeOut(PopupDialog.EXIT_DURATION, Easing.InSine); - lpFilter.CutoffTo(2000, 100, Easing.InCubic).Then().CutoffTo(lpFilter.MaxCutoff); } public override bool OnPressed(KeyBindingPressEvent e) From ffbb7a9b1a45692305c984a1a7fc42d37db2993d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 12:22:32 +0900 Subject: [PATCH 243/391] Remove incorrect csproj change Co-authored-by: Dan Balasescu --- osu.Game/osu.Game.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 05c587bcc0..4877ddf725 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -43,7 +43,4 @@ - - - From 6e797ddcac59f939f7fed68a9f0a91c1bf9a5143 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 12:41:17 +0900 Subject: [PATCH 244/391] Add test coverage of creating, saving and loading a new beatmap --- .../Visual/Editing/TestSceneEditorSaving.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs new file mode 100644 index 0000000000..2258a209e2 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -0,0 +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 NUnit.Framework; +using osu.Framework.Input; +using osu.Framework.Testing; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Menu; +using osu.Game.Screens.Select; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneEditorSaving : OsuGameTestScene + { + private Editor editor => Game.ChildrenOfType().FirstOrDefault(); + + private EditorBeatmap editorBeatmap => (EditorBeatmap)editor.Dependencies.Get(typeof(EditorBeatmap)); + + /// + /// Tests the general expected flow of creating a new beatmap, saving it, then loading it back from song select. + /// + [Test] + public void TestNewBeatmapSaveThenLoad() + { + AddStep("set default beatmap", () => Game.Beatmap.SetDefault()); + + PushAndConfirm(() => new EditorLoader()); + + AddUntilStep("wait for editor load", () => editor != null); + + AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); + + AddStep("Enter compose mode", () => InputManager.Key(Key.F1)); + AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + + AddStep("Change to placement mode", () => InputManager.Key(Key.Number2)); + AddStep("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre)); + AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left)); + + AddStep("Save and exit", () => + { + InputManager.Keys(PlatformAction.Save); + InputManager.Key(Key.Escape); + }); + + AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu); + + PushAndConfirm(() => new PlaySongSelect()); + + AddUntilStep("Wait for beatmap selected", () => !Game.Beatmap.IsDefault); + AddStep("Open options", () => InputManager.Key(Key.F3)); + AddStep("Enter editor", () => InputManager.Key(Key.Number5)); + + AddUntilStep("Wait for editor load", () => editor != null); + AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1); + } + } +} From 007b33cd88d465253613f6d74d05c93c8a6a0882 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 12:05:11 +0900 Subject: [PATCH 245/391] Add missing methods to interfaces --- osu.Game/Beatmaps/BeatmapModelManager.cs | 4 ++-- osu.Game/Beatmaps/IBeatmapModelManager.cs | 20 ++++++++++++++++++++ osu.Game/Beatmaps/IWorkingBeatmapCache.cs | 12 ++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Beatmaps/IBeatmapModelManager.cs diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 250d6653d5..787559899a 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -32,7 +32,7 @@ namespace osu.Game.Beatmaps /// Handles ef-core storage of beatmaps. /// [ExcludeFromDynamicCompile] - public class BeatmapModelManager : ArchiveModelManager + public class BeatmapModelManager : ArchiveModelManager, IBeatmapModelManager { /// /// Fired when a single difficulty has been hidden. @@ -54,7 +54,7 @@ namespace osu.Game.Beatmaps /// /// The game working beatmap cache, used to invalidate entries on changes. /// - public WorkingBeatmapCache WorkingBeatmapCache { private get; set; } + public IWorkingBeatmapCache WorkingBeatmapCache { private get; set; } private readonly Bindable> beatmapRestored = new Bindable>(); diff --git a/osu.Game/Beatmaps/IBeatmapModelManager.cs b/osu.Game/Beatmaps/IBeatmapModelManager.cs new file mode 100644 index 0000000000..8c243c2b77 --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapModelManager.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.Game.Database; + +namespace osu.Game.Beatmaps +{ + public interface IBeatmapModelManager : IModelManager + { + /// + /// Provide an online lookup queue component to handle populating online beatmap metadata. + /// + BeatmapOnlineLookupQueue OnlineLookupQueue { set; } + + /// + /// Provide a working beatmap cache, used to invalidate entries on changes. + /// + IWorkingBeatmapCache WorkingBeatmapCache { set; } + } +} diff --git a/osu.Game/Beatmaps/IWorkingBeatmapCache.cs b/osu.Game/Beatmaps/IWorkingBeatmapCache.cs index 881e734292..3eb33f10d6 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmapCache.cs @@ -11,5 +11,17 @@ namespace osu.Game.Beatmaps /// The beatmap to lookup. /// A instance correlating to the provided . WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo); + + /// + /// Invalidate a cache entry if it exists. + /// + /// The beatmap set info to invalidate any cached entries for. + void Invalidate(BeatmapSetInfo beatmapSetInfo); + + /// + /// Invalidate a cache entry if it exists. + /// + /// The beatmap info to invalidate any cached entries for. + void Invalidate(BeatmapInfo beatmapInfo); } } From 8ffaa491e7303289b35de86eb1fb85e3a9097e81 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 12:05:30 +0900 Subject: [PATCH 246/391] Fix `BeatmapModelManager` not receiving `WorkingBeatmapCache` --- osu.Game/Beatmaps/BeatmapManager.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 91d5b16204..240db22c00 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -45,6 +45,7 @@ namespace osu.Game.Beatmaps workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, resources, new FileStore(contextFactory, storage).Store, defaultBeatmap, host); workingBeatmapCache.BeatmapManager = beatmapModelManager; + beatmapModelManager.WorkingBeatmapCache = workingBeatmapCache; if (performOnlineLookups) { @@ -305,6 +306,9 @@ namespace osu.Game.Beatmaps public WorkingBeatmap GetWorkingBeatmap(BeatmapInfo importedBeatmap) => workingBeatmapCache.GetWorkingBeatmap(importedBeatmap); + void IWorkingBeatmapCache.Invalidate(BeatmapSetInfo beatmapSetInfo) => workingBeatmapCache.Invalidate(beatmapSetInfo); + void IWorkingBeatmapCache.Invalidate(BeatmapInfo beatmapInfo) => workingBeatmapCache.Invalidate(beatmapInfo); + #endregion #region Implementation of IModelFileManager From 90fdaf18c027ac08b3cd15e636105709dd6903e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 12:40:25 +0900 Subject: [PATCH 247/391] Fix `PushAndConfirm` potentially failing if new screen quickly pushes a child screen --- osu.Game/Tests/Visual/OsuGameTestScene.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index 881c4bab02..c025cf85c7 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -83,8 +83,17 @@ namespace osu.Game.Tests.Visual protected void PushAndConfirm(Func newScreen) { Screen screen = null; - AddStep("Push new screen", () => Game.ScreenStack.Push(screen = newScreen())); - AddUntilStep("Wait for new screen", () => Game.ScreenStack.CurrentScreen == screen && screen.IsLoaded); + IScreen previousScreen = null; + + AddStep("Push new screen", () => + { + previousScreen = Game.ScreenStack.CurrentScreen; + Game.ScreenStack.Push(screen = newScreen()); + }); + + AddUntilStep("Wait for new screen", () => screen.IsLoaded + && Game.ScreenStack.CurrentScreen != previousScreen + && previousScreen.GetChildScreen() == screen); } protected void ConfirmAtMainMenu() => AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu menu && menu.IsLoaded); From d9849bcf4995b69bf493d9dcaab88950c40f3a4f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 13:14:39 +0900 Subject: [PATCH 248/391] Fix dragging on an editor file selection text box causing repeated popover display Local fix and no tests as this is a pretty weird usage of `TextBox`. We'll probably want to change it to not use a textbox eventually. Closes #14969. --- osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs index fd43349793..f833bc49f7 100644 --- a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs +++ b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs @@ -89,6 +89,13 @@ namespace osu.Game.Screens.Edit.Setup { public Action OnFocused; + protected override bool OnDragStart(DragStartEvent e) + { + // This text box is intended to be "read only" without actually specifying that. + // As such we don't want to allow the user to select its content with a drag. + return false; + } + protected override void OnFocus(FocusEvent e) { OnFocused?.Invoke(); From b339c149d86b076a34a5af3701eb751a6689034b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 2 Oct 2021 12:34:29 +0900 Subject: [PATCH 249/391] Copy `BaseDifficulty` to `Beatmap` and move all write operations across --- .../CatchSelectionBlueprintTestScene.cs | 2 +- ...TestSceneBananaShowerPlacementBlueprint.cs | 3 +- .../TestSceneJuiceStreamPlacementBlueprint.cs | 11 +++-- .../TestSceneJuiceStreamSelectionBlueprint.cs | 2 +- .../Beatmaps/CatchBeatmapProcessor.cs | 2 +- .../Difficulty/CatchDifficultyCalculator.cs | 6 +-- .../Edit/DrawableCatchEditorRuleset.cs | 2 +- .../UI/DrawableCatchRuleset.cs | 4 +- .../Beatmaps/ManiaBeatmapConverter.cs | 6 +-- .../Legacy/DistanceObjectPatternGenerator.cs | 2 +- .../Patterns/Legacy/PatternGenerator.cs | 2 +- .../Difficulty/ManiaDifficultyCalculator.cs | 2 +- .../UI/DrawableManiaRuleset.cs | 2 +- .../Editor/TestSceneOsuDistanceSnapGrid.cs | 2 +- .../TestSceneGameplayCursor.cs | 4 +- .../TestSceneMissHitWindowJudgements.cs | 4 +- .../TestSceneObjectOrderedHitPolicy.cs | 2 +- .../Difficulty/OsuDifficultyCalculator.cs | 4 +- .../Edit/Checks/CheckTooShortSpinners.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 2 +- .../Replays/OsuAutoGenerator.cs | 2 +- .../Statistics/AccuracyHeatmap.cs | 2 +- .../UI/Cursor/OsuCursorContainer.cs | 2 +- .../Beatmaps/TaikoBeatmapConverter.cs | 14 +++--- .../Difficulty/TaikoDifficultyCalculator.cs | 2 +- .../Scoring/TaikoHealthProcessor.cs | 4 +- .../Formats/LegacyBeatmapDecoderTest.cs | 2 +- .../Beatmaps/Formats/OsuJsonDecoderTest.cs | 4 +- ...tSceneHitObjectComposerDistanceSnapping.cs | 12 ++--- .../TestSceneDrainingHealthProcessor.cs | 4 +- .../TestSceneDrawableScrollingRuleset.cs | 4 +- .../Gameplay/TestSceneGameplayRewinding.cs | 2 +- .../TestScenePlaylistsRoomSubScreen.cs | 4 +- osu.Game/Beatmaps/Beatmap.cs | 49 +++++++++++++++---- .../Beatmaps/Formats/JsonBeatmapDecoder.cs | 2 +- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 6 +-- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 16 +++--- osu.Game/Beatmaps/IBeatmap.cs | 5 ++ osu.Game/Beatmaps/WorkingBeatmap.cs | 6 +-- .../Difficulty/DifficultyCalculator.cs | 7 +++ osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 +- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 2 +- osu.Game/Rulesets/Mods/DifficultyBindable.cs | 2 +- .../Scoring/DrainingHealthProcessor.cs | 2 +- .../UI/Scrolling/DrawableScrollingRuleset.cs | 6 +-- osu.Game/Screens/Edit/EditorBeatmap.cs | 8 ++- .../Screens/Edit/Setup/DifficultySection.cs | 16 +++--- .../Tests/Beatmaps/BeatmapConversionTest.cs | 2 + .../Visual/PlacementBlueprintTestScene.cs | 16 +++--- 49 files changed, 163 insertions(+), 108 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs index a458771550..7d806b119e 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor protected CatchSelectionBlueprintTestScene() { EditorBeatmap = new EditorBeatmap(new CatchBeatmap()); - EditorBeatmap.BeatmapInfo.BaseDifficulty.CircleSize = 0; + EditorBeatmap.Difficulty.CircleSize = 0; EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 100 diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs index e3811b7669..cca3701a60 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneBananaShowerPlacementBlueprint.cs @@ -5,6 +5,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Catch.Edit.Blueprints; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; @@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor protected override void AddHitObject(DrawableHitObject hitObject) { // Create nested bananas (but positions are not randomized because beatmap processing is not done). - hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), Beatmap.Value.BeatmapInfo.BaseDifficulty); + hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); base.AddHitObject(hitObject); } diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs index cd1fa31b61..981efc9a13 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamPlacementBlueprint.cs @@ -4,9 +4,9 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Utils; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Edit.Blueprints; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; @@ -23,11 +23,12 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor private JuiceStream lastObject => LastObject?.HitObject as JuiceStream; - [BackgroundDependencyLoader] - private void load() + protected override IBeatmap GetPlayableBeatmap() { - Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderTickRate = 5; - Beatmap.Value.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity * 10; + var playable = base.GetPlayableBeatmap(); + playable.Difficulty.SliderTickRate = 5; + playable.Difficulty.SliderMultiplier = velocity * 10; + return playable; } [Test] diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs index 5e73a89069..155d033dd0 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/TestSceneJuiceStreamSelectionBlueprint.cs @@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor X = x, Path = sliderPath, }; - EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = velocity; + EditorBeatmap.Difficulty.SliderMultiplier = velocity; EditorBeatmap.Add(hitObject); EditorBeatmap.Update(hitObject); Assert.That(hitObject.Velocity, Is.EqualTo(velocity)); diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index a891ec6c0a..87cc2c45e8 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -211,7 +211,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); - double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2; + double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) / 2; // Todo: This is wrong. osu!stable calculated hyperdashes using the full catcher size, excluding the margins. // This should theoretically cause impossible scenarios, but practically, likely due to the size of the playfield, it doesn't seem possible. diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index 5b1f613f8d..03a76f10ef 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty return new CatchDifficultyAttributes { Mods = mods, Skills = skills }; // this is the same as osu!, so there's potential to share the implementation... maybe - double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; + double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; return new CatchDifficultyAttributes { @@ -69,10 +69,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) { - halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f; + halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.Difficulty) * 0.5f; // For circle sizes above 5.5, reduce the catcher width further to simulate imperfect gameplay. - halfCatcherWidth *= 1 - (Math.Max(0, beatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5.5f) * 0.0625f); + halfCatcherWidth *= 1 - (Math.Max(0, beatmap.Difficulty.CircleSize - 5.5f) * 0.0625f); return new Skill[] { diff --git a/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs index 0344709d45..9a7528d90c 100644 --- a/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs +++ b/osu.Game.Rulesets.Catch/Edit/DrawableCatchEditorRuleset.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Catch.Edit { } - protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.BeatmapInfo.BaseDifficulty); + protected override Playfield CreatePlayfield() => new CatchEditorPlayfield(Beatmap.Difficulty); } } diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index ba6e9224c9..a8ec9f1d2f 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -27,14 +27,14 @@ namespace osu.Game.Rulesets.Catch.UI : base(ruleset, beatmap, mods) { Direction.Value = ScrollingDirection.Down; - TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450); + TimeRange.Value = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450); } protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield); - protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty); + protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.Difficulty); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new CatchPlayfieldAdjustmentContainer(); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 01e44ef9e4..ebfbaccd31 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -42,8 +42,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps { IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo); - var roundedCircleSize = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.CircleSize); - var roundedOverallDifficulty = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + var roundedCircleSize = Math.Round(beatmap.Difficulty.CircleSize); + var roundedOverallDifficulty = Math.Round(beatmap.Difficulty.OverallDifficulty); if (IsForCurrentRuleset) { @@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) { - IBeatmapDifficultyInfo difficulty = original.BeatmapInfo.BaseDifficulty; + 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 FastRandom(seed); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 26e5d381e2..380efff69f 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy StartTime = (int)Math.Round(hitObject.StartTime); // This matches stable's calculation. - EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier); + EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.Difficulty.SliderMultiplier); SegmentDuration = (EndTime - StartTime) / SpanCount; } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs index d65e78bb49..eaf0ea0f2b 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (drainTime == 0) drainTime = 10000; - IBeatmapDifficultyInfo difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty; + 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; conversionDifficulty = Math.Min(conversionDifficulty.Value, 12); diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index fc29eadedc..9140e8afce 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty return new ManiaDifficultyAttributes { Mods = mods, Skills = skills }; HitWindows hitWindows = new ManiaHitWindows(); - hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); return new ManiaDifficultyAttributes { diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 614a7b00c7..3b7da8d9ba 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Mania.UI { // Mania doesn't care about global velocity p.Velocity = 1; - p.BaseBeatLength *= Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier; + p.BaseBeatLength *= Beatmap.Difficulty.SliderMultiplier; // For non-mania beatmap, speed changes should only happen through timing points if (!isForCurrentRuleset) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index 9af2a99470..851be2b2f2 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor [SetUp] public void Setup() => Schedule(() => { - editorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1; + editorBeatmap.Difficulty.SliderMultiplier = 1; editorBeatmap.ControlPointInfo.Clear(); editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length }); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index 41d9bf7132..f09aad8b49 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddSliderStep("circle size", 0f, 10f, 0f, val => { config.SetValue(OsuSetting.AutoCursorSize, true); - gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = val; + gameplayState.Beatmap.Difficulty.CircleSize = val; Scheduler.AddOnce(() => loadContent(false)); }); @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests public void TestSizing(int circleSize, float userScale) { AddStep($"set user scale to {userScale}", () => config.SetValue(OsuSetting.GameplayCursorSize, userScale)); - AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSize); + AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.Difficulty.CircleSize = circleSize); AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true)); AddStep("load content", () => loadContent()); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs index af67ab5839..a5629119b6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests }; var hitWindows = new OsuHitWindows(); - hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); CreateModTest(new ModTestData { @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests }; var hitWindows = new OsuHitWindows(); - hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); CreateModTest(new ModTestData { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index cfce80a2b2..ececfb0586 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -400,9 +400,9 @@ namespace osu.Game.Rulesets.Osu.Tests Beatmap.Value = CreateWorkingBeatmap(new Beatmap { HitObjects = hitObjects, + Difficulty = new BeatmapDifficulty { SliderTickRate = 3 }, BeatmapInfo = { - BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 }, Ruleset = new OsuRuleset().RulesetInfo }, }); diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index a8f10f44dc..17a611817c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; - double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; + double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; int maxCombo = beatmap.HitObjects.Count; // Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above) @@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) { HitWindows hitWindows = new OsuHitWindows(); - hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); // Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate; diff --git a/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs index 0d0c3d9e69..f0aade1b7f 100644 --- a/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs +++ b/osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Checks public IEnumerable Run(BeatmapVerifierContext context) { - double od = context.Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty; + double od = context.Beatmap.Difficulty.OverallDifficulty; // These are meant to reflect the duration necessary for auto to score at least 1000 points on the spinner. // It's difficult to eliminate warnings here, as auto achieving 1000 points depends on the approach angle on some spinners. diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index b0c655b106..bf70a63ab5 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Osu.Mods .Select(beat => { var newCircle = new HitCircle(); - newCircle.ApplyDefaults(controlPointInfo, osuBeatmap.BeatmapInfo.BaseDifficulty); + newCircle.ApplyDefaults(controlPointInfo, osuBeatmap.Difficulty); newCircle.StartTime = beat; return (OsuHitObject)newCircle; }).ToList(); diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index b88bf9108b..e231550e3e 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Replays : base(beatmap, mods) { defaultHitWindows = new OsuHitWindows(); - defaultHitWindows.SetDifficulty(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + defaultHitWindows.SetDifficulty(Beatmap.Difficulty.OverallDifficulty); } #endregion diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index cb769c31b8..24a660e69e 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Osu.Statistics return; // Todo: This should probably not be done like this. - float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5) / 5) / 2; + float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.Difficulty.CircleSize - 5) / 5) / 2; foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle))) { diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs index cfe83d0106..d1d9ee9f4d 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/OsuCursorContainer.cs @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor if (autoCursorScale.Value && state != null) { // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier. - scale *= GetScaleForCircleSize(state.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize); + scale *= GetScaleForCircleSize(state.Beatmap.Difficulty.CircleSize); } cursorScale.Value = scale; diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 3b5b972c01..643a656199 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -46,11 +46,11 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) { - if (!(original.BeatmapInfo.BaseDifficulty is TaikoMutliplierAppliedDifficulty)) + if (!(original.Difficulty is TaikoMutliplierAppliedDifficulty)) { // Rewrite the beatmap info to add the slider velocity multiplier original.BeatmapInfo = original.BeatmapInfo.Clone(); - original.BeatmapInfo.BaseDifficulty = new TaikoMutliplierAppliedDifficulty(original.BeatmapInfo.BaseDifficulty); + original.Difficulty = new TaikoMutliplierAppliedDifficulty(original.Difficulty); } Beatmap converted = base.ConvertBeatmap(original, cancellationToken); @@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps StartTime = obj.StartTime, Samples = obj.Samples, Duration = taikoDuration, - TickRate = beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate == 3 ? 3 : 4 + TickRate = beatmap.Difficulty.SliderTickRate == 3 ? 3 : 4 }; } @@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps case IHasDuration endTimeData: { - double hitMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier; + double hitMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier; yield return new Swell { @@ -164,10 +164,10 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps else beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier; - double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate; + double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate; // The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll. - double taikoVelocity = sliderScoringPointDistance * beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate; + double taikoVelocity = sliderScoringPointDistance * beatmap.Difficulty.SliderTickRate; taikoDuration = (int)(distance / taikoVelocity * beatLength); if (isForCurrentRuleset) @@ -183,7 +183,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps beatLength = timingPoint.BeatLength; // If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat - tickSpacing = Math.Min(beatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, (double)taikoDuration / spans); + tickSpacing = Math.Min(beatLength / beatmap.Difficulty.SliderTickRate, (double)taikoDuration / spans); return tickSpacing > 0 && distance / osuVelocity * 1000 < 2 * beatLength; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 18d06c069f..e755bb2325 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty starRating = rescale(starRating); HitWindows hitWindows = new TaikoHitWindows(); - hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); return new TaikoDifficultyAttributes { diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index 94cd411d7b..0d6ce44255 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Taiko.Scoring { base.ApplyBeatmap(beatmap); - hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType().Count()) * IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); - hpMissMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120); + hpMultiplier = 1 / (object_count_factor * Math.Max(1, beatmap.HitObjects.OfType().Count()) * IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.5, 0.75, 0.98)); + hpMissMultiplier = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, 0.0018, 0.0075, 0.0120); } protected override double GetHealthIncreaseFor(JudgementResult result) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index a4bf8c92e3..b5e1fa204f 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -129,7 +129,7 @@ namespace osu.Game.Tests.Beatmaps.Formats using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var stream = new LineBufferedReader(resStream)) { - var difficulty = decoder.Decode(stream).BeatmapInfo.BaseDifficulty; + var difficulty = decoder.Decode(stream).Difficulty; Assert.AreEqual(6.5f, difficulty.DrainRate); Assert.AreEqual(4, difficulty.CircleSize); diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index 1fc3abef9a..9ec2f37569 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -84,7 +84,7 @@ namespace osu.Game.Tests.Beatmaps.Formats public void TestDecodeDifficulty() { var beatmap = decodeAsJson(normal); - var difficulty = beatmap.BeatmapInfo.BaseDifficulty; + var difficulty = beatmap.Difficulty; Assert.AreEqual(6.5f, difficulty.DrainRate); Assert.AreEqual(4, difficulty.CircleSize); Assert.AreEqual(8, difficulty.OverallDifficulty); @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Beatmaps.Formats processor.PreProcess(); foreach (var o in converted.HitObjects) - o.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty); + o.ApplyDefaults(converted.ControlPointInfo, converted.Difficulty); processor.PostProcess(); var beatmap = converted.Serialize().Deserialize(); diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index bd34eaff63..a40a6dac4c 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Editing BeatDivisor.Value = 1; - composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 1; + composer.EditorBeatmap.Difficulty.SliderMultiplier = 1; composer.EditorBeatmap.ControlPointInfo.Clear(); composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 1 }); @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Editing [TestCase(2)] public void TestSliderMultiplier(float multiplier) { - AddStep($"set multiplier = {multiplier}", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = multiplier); + AddStep($"set multiplier = {multiplier}", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = multiplier); assertSnapDistance(100 * multiplier); } @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Editing assertDurationToDistance(500, 50); assertDurationToDistance(1000, 100); - AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); + AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2); assertDurationToDistance(500, 100); assertDurationToDistance(1000, 200); @@ -118,7 +118,7 @@ namespace osu.Game.Tests.Editing assertDistanceToDuration(50, 500); assertDistanceToDuration(100, 1000); - AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); + AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2); assertDistanceToDuration(100, 500); assertDistanceToDuration(200, 1000); @@ -143,7 +143,7 @@ namespace osu.Game.Tests.Editing assertSnappedDuration(200, 2000); assertSnappedDuration(250, 3000); - AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); + AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2); assertSnappedDuration(0, 0); assertSnappedDuration(50, 0); @@ -175,7 +175,7 @@ namespace osu.Game.Tests.Editing assertSnappedDistance(200, 200); assertSnappedDistance(250, 200); - AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); + AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.Difficulty.SliderMultiplier = 2); assertSnappedDistance(50, 0); assertSnappedDistance(100, 0); diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index 7264083338..296c5cef76 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -165,7 +165,7 @@ namespace osu.Game.Tests.Gameplay { var beatmap = new Beatmap { - BeatmapInfo = { BaseDifficulty = { DrainRate = 10 } }, + Difficulty = { DrainRate = 10 } }; beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = 0 }); @@ -200,7 +200,7 @@ namespace osu.Game.Tests.Gameplay { var beatmap = new Beatmap { - BeatmapInfo = { BaseDifficulty = { DrainRate = 10 } }, + Difficulty = { DrainRate = 10 } }; for (double time = startTime; time <= endTime; time += 100) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index 75a5eec6f7..3f10d7892d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -182,7 +182,7 @@ namespace osu.Game.Tests.Visual.Gameplay { var beatmap = createBeatmap(); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range }); - beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2; + beatmap.Difficulty.SliderMultiplier = 2; createTest(beatmap, d => d.RelativeScaleBeatLengthsOverride = true); AddStep("adjust time range", () => drawableRuleset.TimeRange.Value = 5000); @@ -196,7 +196,7 @@ namespace osu.Game.Tests.Visual.Gameplay { var beatmap = createBeatmap(); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range }); - beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2; + beatmap.Difficulty.SliderMultiplier = 2; createTest(beatmap); AddStep("adjust time range", () => drawableRuleset.TimeRange.Value = 2000); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs index 73c6970482..814b41cdbc 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Visual.Gameplay { var beatmap = new Beatmap { - BeatmapInfo = { BaseDifficulty = { ApproachRate = 9 } }, + Difficulty = { ApproachRate = 9 }, }; for (int i = 0; i < 15; i++) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index d8ec89a94e..deb9a22184 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -137,11 +137,11 @@ namespace osu.Game.Tests.Visual.Playlists InputManager.Click(MouseButton.Left); }); - AddAssert("match has altered beatmap", () => match.Beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize == 1); + AddAssert("match has altered beatmap", () => match.Beatmap.Value.Beatmap.Difficulty.CircleSize == 1); AddStep("re-import original beatmap", () => manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait()); - AddAssert("match has original beatmap", () => match.Beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize != 1); + AddAssert("match has original beatmap", () => match.Beatmap.Value.Beatmap.Difficulty.CircleSize != 1); } private class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index e5b6a4bc44..f2a60c4711 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -18,17 +18,48 @@ namespace osu.Game.Beatmaps public class Beatmap : IBeatmap where T : HitObject { - public BeatmapInfo BeatmapInfo { get; set; } = new BeatmapInfo + private BeatmapDifficulty difficulty = new BeatmapDifficulty(); + + public BeatmapDifficulty Difficulty { - Metadata = new BeatmapMetadata + get => difficulty; + set { - Artist = @"Unknown", - Title = @"Unknown", - AuthorString = @"Unknown Creator", - }, - Version = @"Normal", - BaseDifficulty = new BeatmapDifficulty() - }; + difficulty = value; + + if (beatmapInfo != null) + beatmapInfo.BaseDifficulty = new BeatmapDifficulty(difficulty); + } + } + + private BeatmapInfo beatmapInfo; + + public BeatmapInfo BeatmapInfo + { + get => beatmapInfo; + set + { + beatmapInfo = value; + + if (beatmapInfo != null) + Difficulty = new BeatmapDifficulty(beatmapInfo.BaseDifficulty); + } + } + + public Beatmap() + { + beatmapInfo = new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Artist = @"Unknown", + Title = @"Unknown", + AuthorString = @"Unknown Creator", + }, + Version = @"Normal", + BaseDifficulty = Difficulty, + }; + } [JsonIgnore] public BeatmapMetadata Metadata => BeatmapInfo?.Metadata ?? BeatmapInfo?.BeatmapSet?.Metadata; diff --git a/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs index 988968fa42..0d5c48f64d 100644 --- a/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs @@ -18,7 +18,7 @@ namespace osu.Game.Beatmaps.Formats stream.ReadToEnd().DeserializeInto(output); foreach (var hitObject in output.HitObjects) - hitObject.ApplyDefaults(output.ControlPointInfo, output.BeatmapInfo.BaseDifficulty); + hitObject.ApplyDefaults(output.ControlPointInfo, output.Difficulty); } } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 4b5eaafa4a..f71b148008 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -67,7 +67,7 @@ namespace osu.Game.Beatmaps.Formats this.beatmap.HitObjects = this.beatmap.HitObjects.OrderBy(h => h.StartTime).ToList(); foreach (var hitObject in this.beatmap.HitObjects) - hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.BeatmapInfo.BaseDifficulty); + hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.Difficulty); } protected override bool ShouldSkipLine(string line) => base.ShouldSkipLine(line) || line.StartsWith(' ') || line.StartsWith('_'); @@ -276,7 +276,7 @@ namespace osu.Game.Beatmaps.Formats { var pair = SplitKeyVal(line); - var difficulty = beatmap.BeatmapInfo.BaseDifficulty; + var difficulty = beatmap.Difficulty; switch (pair.Key) { @@ -444,7 +444,7 @@ namespace osu.Game.Beatmaps.Formats if (obj != null) { - obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty); + obj.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); beatmap.HitObjects.Add(obj); } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index aef13b8872..74b3c178cd 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -141,17 +141,17 @@ namespace osu.Game.Beatmaps.Formats { writer.WriteLine("[Difficulty]"); - writer.WriteLine(FormattableString.Invariant($"HPDrainRate: {beatmap.BeatmapInfo.BaseDifficulty.DrainRate}")); - writer.WriteLine(FormattableString.Invariant($"CircleSize: {beatmap.BeatmapInfo.BaseDifficulty.CircleSize}")); - writer.WriteLine(FormattableString.Invariant($"OverallDifficulty: {beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty}")); - writer.WriteLine(FormattableString.Invariant($"ApproachRate: {beatmap.BeatmapInfo.BaseDifficulty.ApproachRate}")); + writer.WriteLine(FormattableString.Invariant($"HPDrainRate: {beatmap.Difficulty.DrainRate}")); + writer.WriteLine(FormattableString.Invariant($"CircleSize: {beatmap.Difficulty.CircleSize}")); + writer.WriteLine(FormattableString.Invariant($"OverallDifficulty: {beatmap.Difficulty.OverallDifficulty}")); + writer.WriteLine(FormattableString.Invariant($"ApproachRate: {beatmap.Difficulty.ApproachRate}")); // Taiko adjusts the slider multiplier (see: LEGACY_TAIKO_VELOCITY_MULTIPLIER) writer.WriteLine(beatmap.BeatmapInfo.RulesetID == 1 - ? FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / LEGACY_TAIKO_VELOCITY_MULTIPLIER}") - : FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier}")); + ? FormattableString.Invariant($"SliderMultiplier: {beatmap.Difficulty.SliderMultiplier / LEGACY_TAIKO_VELOCITY_MULTIPLIER}") + : FormattableString.Invariant($"SliderMultiplier: {beatmap.Difficulty.SliderMultiplier}")); - writer.WriteLine(FormattableString.Invariant($"SliderTickRate: {beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate}")); + writer.WriteLine(FormattableString.Invariant($"SliderTickRate: {beatmap.Difficulty.SliderTickRate}")); } private void handleEvents(TextWriter writer) @@ -285,7 +285,7 @@ namespace osu.Game.Beatmaps.Formats break; case 3: - int totalColumns = (int)Math.Max(1, beatmap.BeatmapInfo.BaseDifficulty.CircleSize); + int totalColumns = (int)Math.Max(1, beatmap.Difficulty.CircleSize); position.X = (int)Math.Ceiling(((IHasXPosition)hitObject).X * (512f / totalColumns)); break; } diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index f61dd269e1..3f598cd1e5 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -20,6 +20,11 @@ namespace osu.Game.Beatmaps /// BeatmapMetadata Metadata { get; } + /// + /// This beatmap's difficulty settings. + /// + public BeatmapDifficulty Difficulty { get; set; } + /// /// The control points in this beatmap. /// diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index c4c5c89f28..c7ccfe976a 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -120,14 +120,14 @@ namespace osu.Game.Beatmaps if (mods.Any(m => m is IApplicableToDifficulty)) { converted.BeatmapInfo = converted.BeatmapInfo.Clone(); - converted.BeatmapInfo.BaseDifficulty = converted.BeatmapInfo.BaseDifficulty.Clone(); + converted.Difficulty = converted.Difficulty.Clone(); foreach (var mod in mods.OfType()) { if (cancellationSource.IsCancellationRequested) throw new BeatmapLoadTimeoutException(BeatmapInfo); - mod.ApplyToDifficulty(converted.BeatmapInfo.BaseDifficulty); + mod.ApplyToDifficulty(converted.Difficulty); } } @@ -146,7 +146,7 @@ namespace osu.Game.Beatmaps if (cancellationSource.IsCancellationRequested) throw new BeatmapLoadTimeoutException(BeatmapInfo); - obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty, cancellationSource.Token); + obj.ApplyDefaults(converted.ControlPointInfo, converted.Difficulty, cancellationSource.Token); } } catch (OperationCanceledException) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index a7c4790366..7e3e5d5417 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -257,6 +257,13 @@ namespace osu.Game.Rulesets.Difficulty } public BeatmapMetadata Metadata => baseBeatmap.Metadata; + + public BeatmapDifficulty Difficulty + { + get => baseBeatmap.Difficulty; + set => baseBeatmap.Difficulty = value; + } + public List Breaks => baseBeatmap.Breaks; public double TotalBreakTime => baseBeatmap.TotalBreakTime; public IEnumerable GetStatistics() => baseBeatmap.GetStatistics(); diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 8090fcbd32..b41e0442bc 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -392,7 +392,7 @@ namespace osu.Game.Rulesets.Edit public override float GetBeatSnapDistanceAt(double referenceTime) { DifficultyControlPoint difficultyPoint = EditorBeatmap.ControlPointInfo.DifficultyPointAt(referenceTime); - return (float)(100 * EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / BeatSnapProvider.BeatDivisor); + return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / BeatSnapProvider.BeatDivisor); } public override float DurationToDistance(double referenceTime, double duration) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index b7529f39ca..0c0c5990d1 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -112,7 +112,7 @@ namespace osu.Game.Rulesets.Edit /// Invokes , /// refreshing and parameters for the . /// - protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty); + protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent?.ReceivePositionalInputAt(screenSpacePos) ?? false; diff --git a/osu.Game/Rulesets/Mods/DifficultyBindable.cs b/osu.Game/Rulesets/Mods/DifficultyBindable.cs index e4304795f2..6cfae0b085 100644 --- a/osu.Game/Rulesets/Mods/DifficultyBindable.cs +++ b/osu.Game/Rulesets/Mods/DifficultyBindable.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mods /// /// A function that can extract the current value of this setting from a beatmap difficulty for display purposes. /// - public Func ReadCurrentFromDifficulty; + public Func ReadCurrentFromDifficulty; public float Precision { diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 85693abb93..dfeb6b4788 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Scoring .First() ))); - targetMinimumHealth = IBeatmapDifficultyInfo.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, min_health_target, mid_health_target, max_health_target); + targetMinimumHealth = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.DrainRate, min_health_target, mid_health_target, max_health_target); // Add back a portion of the amount of HP to be drained, depending on the lenience requested. targetMinimumHealth += drainLenience * (1 - targetMinimumHealth); diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs index 7b30bb9574..041c5ebef5 100644 --- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs +++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs @@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.UI.Scrolling maxDuration = duration; // The slider multiplier is post-multiplied to determine the final velocity, but for relative scale beat lengths // the multiplier should not affect the effective timing point (the longest in the beatmap), so it is factored out here - baseBeatLength = timingPoints[i].BeatLength / Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier; + baseBeatLength = timingPoints[i].BeatLength / Beatmap.Difficulty.SliderMultiplier; } } } @@ -155,7 +155,7 @@ namespace osu.Game.Rulesets.UI.Scrolling return new MultiplierControlPoint(c.Time) { - Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier, + Velocity = Beatmap.Difficulty.SliderMultiplier, BaseBeatLength = baseBeatLength, TimingPoint = lastTimingPoint, DifficultyPoint = lastDifficultyPoint @@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.UI.Scrolling ControlPoints.AddRange(timingChanges); if (ControlPoints.Count == 0) - ControlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier }); + ControlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.Difficulty.SliderMultiplier }); } protected override void LoadComplete() diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 3402bf653a..64eb6225fa 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -86,6 +86,12 @@ namespace osu.Game.Screens.Edit public BeatmapMetadata Metadata => PlayableBeatmap.Metadata; + public BeatmapDifficulty Difficulty + { + get => PlayableBeatmap.Difficulty; + set => PlayableBeatmap.Difficulty = value; + } + public ControlPointInfo ControlPointInfo { get => PlayableBeatmap.ControlPointInfo; @@ -286,7 +292,7 @@ namespace osu.Game.Screens.Edit /// public void Clear() => RemoveRange(HitObjects.ToArray()); - private void processHitObject(HitObject hitObject) => hitObject.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty); + private void processHitObject(HitObject hitObject) => hitObject.ApplyDefaults(ControlPointInfo, PlayableBeatmap.Difficulty); private void trackStartTime(HitObject hitObject) { diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index a8800d524f..75c6a89a66 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Edit.Setup Label = "Object Size", FixedLabelWidth = LABEL_WIDTH, Description = "The size of all hit objects", - Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.CircleSize) + Current = new BindableFloat(Beatmap.Difficulty.CircleSize) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Edit.Setup Label = "Health Drain", FixedLabelWidth = LABEL_WIDTH, Description = "The rate of passive health drain throughout playable time", - Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.DrainRate) + Current = new BindableFloat(Beatmap.Difficulty.DrainRate) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, @@ -56,7 +56,7 @@ namespace osu.Game.Screens.Edit.Setup Label = "Approach Rate", FixedLabelWidth = LABEL_WIDTH, Description = "The speed at which objects are presented to the player", - Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate) + Current = new BindableFloat(Beatmap.Difficulty.ApproachRate) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Edit.Setup Label = "Overall Difficulty", FixedLabelWidth = LABEL_WIDTH, Description = "The harshness of hit windows and difficulty of special objects (ie. spinners)", - Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty) + Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, @@ -87,10 +87,10 @@ 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.BeatmapInfo.BaseDifficulty.CircleSize = circleSizeSlider.Current.Value; - Beatmap.BeatmapInfo.BaseDifficulty.DrainRate = healthDrainSlider.Current.Value; - Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate = approachRateSlider.Current.Value; - Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty = overallDifficultySlider.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.UpdateAllHitObjects(); } diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index 64f1ee4a7a..54fb287661 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -111,6 +111,8 @@ namespace osu.Game.Tests.Beatmaps var converterResult = new Dictionary>(); + beatmap.BeatmapInfo.BaseDifficulty = beatmap.Difficulty; + var working = new ConversionWorkingBeatmap(beatmap) { ConversionGenerated = (o, r, c) => diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index 42cf826bd4..b2f5b1754f 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -24,24 +25,25 @@ namespace osu.Game.Tests.Visual base.Content.Add(HitObjectContainer = CreateHitObjectContainer().With(c => c.Clock = new FramedClock(new StopwatchClock()))); } - [BackgroundDependencyLoader] - private void load() - { - Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize = 2; - } - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies.CacheAs(new EditorClock()); - var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); + var playable = GetPlayableBeatmap(); dependencies.CacheAs(new EditorBeatmap(playable)); return dependencies; } + protected virtual IBeatmap GetPlayableBeatmap() + { + var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); + playable.Difficulty.CircleSize = 2; + return playable; + } + protected override void LoadComplete() { base.LoadComplete(); From 0ab8dcc2a0cc227c22f3a2d6eb9fa8756a2d542c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 15:10:42 +0900 Subject: [PATCH 250/391] Fix taiko weird difficulty multiplier failing on double convert --- .../Beatmaps/TaikoBeatmapConverter.cs | 27 ++++++++++++++-- osu.Game/Beatmaps/Beatmap.cs | 6 ++-- osu.Game/Beatmaps/BeatmapDifficulty.cs | 31 ++++++++++--------- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 643a656199..ef8cf5cb53 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Utils; using System.Threading; +using JetBrains.Annotations; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Formats; @@ -49,7 +50,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps if (!(original.Difficulty is TaikoMutliplierAppliedDifficulty)) { // Rewrite the beatmap info to add the slider velocity multiplier - original.BeatmapInfo = original.BeatmapInfo.Clone(); original.Difficulty = new TaikoMutliplierAppliedDifficulty(original.Difficulty); } @@ -196,9 +196,30 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps public TaikoMutliplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty) { CopyFrom(difficulty); - - SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; } + + [UsedImplicitly] + public TaikoMutliplierAppliedDifficulty() + { + } + + #region Overrides of BeatmapDifficulty + + public override void CopyTo(BeatmapDifficulty other) + { + base.CopyTo(other); + if (!(other is TaikoMutliplierAppliedDifficulty)) + SliderMultiplier /= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; + } + + public override void CopyFrom(IBeatmapDifficultyInfo other) + { + base.CopyFrom(other); + if (!(other is TaikoMutliplierAppliedDifficulty)) + SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; + } + + #endregion } } } diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index f2a60c4711..b2211e26cf 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -28,7 +28,7 @@ namespace osu.Game.Beatmaps difficulty = value; if (beatmapInfo != null) - beatmapInfo.BaseDifficulty = new BeatmapDifficulty(difficulty); + beatmapInfo.BaseDifficulty = difficulty.Clone(); } } @@ -41,8 +41,8 @@ namespace osu.Game.Beatmaps { beatmapInfo = value; - if (beatmapInfo != null) - Difficulty = new BeatmapDifficulty(beatmapInfo.BaseDifficulty); + if (beatmapInfo?.BaseDifficulty != null) + Difficulty = beatmapInfo.BaseDifficulty.Clone(); } } diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 2bb0787b4c..dfd21469fa 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.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.Game.Database; namespace osu.Game.Beatmaps @@ -43,31 +44,31 @@ namespace osu.Game.Beatmaps /// public BeatmapDifficulty Clone() { - var diff = new BeatmapDifficulty(); + var diff = (BeatmapDifficulty)Activator.CreateInstance(GetType()); CopyTo(diff); return diff; } - public void CopyFrom(IBeatmapDifficultyInfo difficulty) + public virtual void CopyFrom(IBeatmapDifficultyInfo other) { - ApproachRate = difficulty.ApproachRate; - DrainRate = difficulty.DrainRate; - CircleSize = difficulty.CircleSize; - OverallDifficulty = difficulty.OverallDifficulty; + ApproachRate = other.ApproachRate; + DrainRate = other.DrainRate; + CircleSize = other.CircleSize; + OverallDifficulty = other.OverallDifficulty; - SliderMultiplier = difficulty.SliderMultiplier; - SliderTickRate = difficulty.SliderTickRate; + SliderMultiplier = other.SliderMultiplier; + SliderTickRate = other.SliderTickRate; } - public void CopyTo(BeatmapDifficulty difficulty) + public virtual void CopyTo(BeatmapDifficulty other) { - difficulty.ApproachRate = ApproachRate; - difficulty.DrainRate = DrainRate; - difficulty.CircleSize = CircleSize; - difficulty.OverallDifficulty = OverallDifficulty; + other.ApproachRate = ApproachRate; + other.DrainRate = DrainRate; + other.CircleSize = CircleSize; + other.OverallDifficulty = OverallDifficulty; - difficulty.SliderMultiplier = SliderMultiplier; - difficulty.SliderTickRate = SliderTickRate; + other.SliderMultiplier = SliderMultiplier; + other.SliderTickRate = SliderTickRate; } } } From 222997f2b9bce92d37364d188d115f8a0995cfaf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 15:18:21 +0900 Subject: [PATCH 251/391] Copy difficulty settings back out to `BeatmapInfo` on editor changes --- osu.Game/Screens/Edit/Editor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 2ff0101dc0..b8a2d3612e 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -332,6 +332,8 @@ namespace osu.Game.Screens.Edit // no longer new after first user-triggered save. isNewBeatmap = false; + playableBeatmap.BeatmapInfo.BaseDifficulty.CopyFrom(playableBeatmap.Difficulty); + // apply any set-level metadata changes. beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet); From 3803f2f4624293b7c948a4ee9b6eb3211572a853 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 16:07:27 +0900 Subject: [PATCH 252/391] Fix leaderboard potentially displaying the wrong scores Closes #14762. This class is ugly. I think the whole process should be clened up once we have correctly-scheduled `SynchronizationContext`s. There's not much saving it as long as all these interdispersed `Schedule`s around required. --- osu.Game/Online/Leaderboards/Leaderboard.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 4f8b27602b..e3ac9f603d 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -255,6 +255,7 @@ namespace osu.Game.Online.Leaderboards } private APIRequest getScoresRequest; + private ScheduledDelegate getScoresRequestCallback; protected abstract bool IsOnlineScope { get; } @@ -282,13 +283,16 @@ namespace osu.Game.Online.Leaderboards getScoresRequest?.Cancel(); getScoresRequest = null; + getScoresRequestCallback?.Cancel(); + getScoresRequestCallback = null; + pendingUpdateScores?.Cancel(); pendingUpdateScores = Schedule(() => { PlaceholderState = PlaceholderState.Retrieving; loading.Show(); - getScoresRequest = FetchScores(scores => Schedule(() => + getScoresRequest = FetchScores(scores => getScoresRequestCallback = Schedule(() => { Scores = scores.ToArray(); PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores; @@ -297,7 +301,7 @@ namespace osu.Game.Online.Leaderboards if (getScoresRequest == null) return; - getScoresRequest.Failure += e => Schedule(() => + getScoresRequest.Failure += e => getScoresRequestCallback = Schedule(() => { if (e is OperationCanceledException) return; From 456cfd62bf50fb58da56594555fa338be0380f14 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 6 Oct 2021 16:46:24 +0900 Subject: [PATCH 253/391] Fix intermittent score panel test failure --- osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs index 6f3b3028be..b7b7407428 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs @@ -221,6 +221,8 @@ namespace osu.Game.Tests.Visual.Ranking list.SelectedScore.Value = middleScore; }); + AddUntilStep("wait for all scores to be visible", () => list.ChildrenOfType().All(t => t.IsPresent)); + assertScoreState(highestScore, false); assertScoreState(middleScore, true); assertScoreState(lowestScore, false); From fc0c3e8758a5e3817881069da5a26e27b80c557e Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 6 Oct 2021 17:32:28 +0900 Subject: [PATCH 254/391] Attach filters at load time --- osu.Game.Tests/Visual/Audio/TestSceneFilter.cs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Audio/TestSceneFilter.cs b/osu.Game.Tests/Visual/Audio/TestSceneFilter.cs index 79d8f7da8c..c639da63d3 100644 --- a/osu.Game.Tests/Visual/Audio/TestSceneFilter.cs +++ b/osu.Game.Tests/Visual/Audio/TestSceneFilter.cs @@ -18,7 +18,6 @@ namespace osu.Game.Tests.Visual.Audio public class TestSceneFilter : OsuTestScene { private WorkingBeatmap testBeatmap; - private OsuSpriteText lowpassText; private OsuSpriteText highpassText; private Filter lowpassFilter; @@ -28,12 +27,12 @@ namespace osu.Game.Tests.Visual.Audio private void load(AudioManager audio) { testBeatmap = new WaveformTestBeatmap(audio); - lowpassFilter = new Filter(audio.TrackMixer); - highpassFilter = new Filter(audio.TrackMixer, BQFType.HighPass); Add(new FillFlowContainer { Children = new Drawable[] { + lowpassFilter = new Filter(audio.TrackMixer), + highpassFilter = new Filter(audio.TrackMixer, BQFType.HighPass), lowpassText = new OsuSpriteText { Padding = new MarginPadding(20), @@ -74,15 +73,8 @@ namespace osu.Game.Tests.Visual.Audio private void testFilter(Filter filter, int cutoffFrom, int cutoffTo) { - Add(filter); - AddStep("Prepare Track", () => - { - testBeatmap.LoadTrack(); - }); - AddStep("Play Track", () => - { - testBeatmap.Track.Start(); - }); + AddStep("Load Track", () => testBeatmap.LoadTrack()); + AddStep("Play Track", () => testBeatmap.Track.Start()); AddWaitStep("Let track play", 10); AddStep("Filter Sweep", () => { From bd0c98614435d91526409b78f1de14f18a00a3da Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 6 Oct 2021 17:34:24 +0900 Subject: [PATCH 255/391] Add asserts to ensure correct state when attaching/detaching the filter --- osu.Game/Audio/Effects/Filter.cs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/osu.Game/Audio/Effects/Filter.cs b/osu.Game/Audio/Effects/Filter.cs index 00b617ffa1..2036851cad 100644 --- a/osu.Game/Audio/Effects/Filter.cs +++ b/osu.Game/Audio/Effects/Filter.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 ManagedBass.Fx; using osu.Framework.Audio.Mixing; using osu.Framework.Bindables; @@ -44,7 +45,7 @@ namespace osu.Game.Audio.Effects break; } - Cutoff = new BindableNumber + Cutoff = new BindableNumber(initialCutoff) { MinValue = 1, MaxValue = MaxCutoff @@ -57,14 +58,24 @@ namespace osu.Game.Audio.Effects fQ = 0.7f // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0) }; - attachFilter(); + // Don't start attached if this is low-pass or high-pass filter (as they have special auto-attach/detach logic) + if (type != BQFType.LowPass && type != BQFType.HighPass) + attachFilter(); + Cutoff.ValueChanged += updateFilter; - Cutoff.Value = initialCutoff; } - private void attachFilter() => mixer.Effects.Add(filter); + private void attachFilter() + { + Debug.Assert(!mixer.Effects.Contains(filter)); + mixer.Effects.Add(filter); + } - private void detachFilter() => mixer.Effects.Remove(filter); + private void detachFilter() + { + Debug.Assert(mixer.Effects.Contains(filter)); + mixer.Effects.Remove(filter); + } private void updateFilter(ValueChangedEvent cutoff) { From 99fb86878e42a75d162a2742b45f3c6f9779f79b Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 6 Oct 2021 18:31:56 +0900 Subject: [PATCH 256/391] Only detach filter on disposal if attached --- osu.Game/Audio/Effects/Filter.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Audio/Effects/Filter.cs b/osu.Game/Audio/Effects/Filter.cs index 2036851cad..428a69bb19 100644 --- a/osu.Game/Audio/Effects/Filter.cs +++ b/osu.Game/Audio/Effects/Filter.cs @@ -118,7 +118,9 @@ namespace osu.Game.Audio.Effects protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - detachFilter(); + + if (mixer.Effects.Contains(filter)) + detachFilter(); } } } From 34269d48e5e41a26d2e94e6b367ee6a583cf649e Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Wed, 6 Oct 2021 12:25:19 +0100 Subject: [PATCH 257/391] Use global multipliers instead of multiplying skill values --- .../Difficulty/OsuPerformanceCalculator.cs | 26 +++++-------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 62e0dea4fa..9d5887c26e 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -40,11 +40,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); - if (mods.Any(h => h is OsuModRelax)) - { - countMiss += countOk + countMeh; - } - // Custom multipliers for NoFail and SpunOut. double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. @@ -54,6 +49,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (mods.Any(m => m is OsuModSpunOut)) multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85); + if (mods.Any(h => h is OsuModRelax)) + { + countMiss += countOk + countMeh; + multiplier *= 0.6; + } + double aimValue = computeAimValue(); double speedValue = computeSpeedValue(); double accuracyValue = computeAccuracyValue(); @@ -95,11 +96,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= lengthBonus; - if (mods.Any(h => h is OsuModRelax)) - { - aimValue *= 0.6; - } - // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (countMiss > 0) aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss); @@ -136,11 +132,6 @@ 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; - if (mods.Any(h => h is OsuModRelax)) - { - speedValue *= 0.6; - } - // Longer maps are worth more. double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); @@ -222,11 +213,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (mods.Any(h => h is OsuModHidden)) flashlightValue *= 1.3; - if (mods.Any(h => h is OsuModRelax)) - { - flashlightValue *= 0.6; - } - // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (countMiss > 0) flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875)); From 433e7cd4030ff1e2bd428b3112c23934369afccf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 21:26:30 +0900 Subject: [PATCH 258/391] Fix rate mods not working if pp counter is displayed --- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 5 +++-- .../Screens/Play/HUD/PerformancePointsCounter.cs | 12 ++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index a7c4790366..1143549f7f 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -110,10 +110,11 @@ namespace osu.Game.Rulesets.Difficulty private void preProcess(Mod[] mods) { playableMods = mods.Select(m => m.DeepClone()).ToArray(); - Beatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods); + + Beatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, playableMods); var track = new TrackVirtual(10000); - mods.OfType().ForEach(m => m.ApplyToTrack(track)); + playableMods.OfType().ForEach(m => m.ApplyToTrack(track)); clockRate = track.Rate; } diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 2ae7b5660a..15d9f9517b 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -60,6 +60,8 @@ namespace osu.Game.Screens.Play.HUD Current.Value = DisplayedCount = 0; } + private Mod[] clonedMods; + [BackgroundDependencyLoader] private void load(OsuColour colours, BeatmapDifficultyCache difficultyCache) { @@ -67,8 +69,10 @@ namespace osu.Game.Screens.Play.HUD if (gameplayState != null) { + clonedMods = gameplayState.Mods.Select(m => m.DeepClone()).ToArray(); + var gameplayWorkingBeatmap = new GameplayWorkingBeatmap(gameplayState.Beatmap); - difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, gameplayState.Mods.ToArray(), loadCancellationSource.Token) + difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, gameplayState.Mods.Select(m => m.DeepClone()).ToArray(), loadCancellationSource.Token) .ContinueWith(r => Schedule(() => { timedAttributes = r.Result; @@ -116,7 +120,11 @@ namespace osu.Game.Screens.Play.HUD return; } - var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(attrib, gameplayState.Score.ScoreInfo); + // awkward but we need to make sure the true mods are not passed to PerformanceCalculator as it makes a mess of track applications. + var scoreInfo = gameplayState.Score.ScoreInfo.DeepClone(); + scoreInfo.Mods = clonedMods; + + var calculator = gameplayState.Ruleset.CreatePerformanceCalculator(attrib, scoreInfo); Current.Value = (int)Math.Round(calculator?.Calculate() ?? 0, MidpointRounding.AwayFromZero); IsValid = true; From 9705c7b546ff0ebe61e7bae5bd5ea8e3fdbcadd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Oct 2021 21:30:30 +0900 Subject: [PATCH 259/391] Use cloned mods in one more place --- osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs index 15d9f9517b..ef289c2a20 100644 --- a/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs +++ b/osu.Game/Screens/Play/HUD/PerformancePointsCounter.cs @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Play.HUD clonedMods = gameplayState.Mods.Select(m => m.DeepClone()).ToArray(); var gameplayWorkingBeatmap = new GameplayWorkingBeatmap(gameplayState.Beatmap); - difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, gameplayState.Mods.Select(m => m.DeepClone()).ToArray(), loadCancellationSource.Token) + difficultyCache.GetTimedDifficultyAttributesAsync(gameplayWorkingBeatmap, gameplayState.Ruleset, clonedMods, loadCancellationSource.Token) .ContinueWith(r => Schedule(() => { timedAttributes = r.Result; From d6eab02d9273ff87cd648db7d2b4d2b278813ef9 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Wed, 6 Oct 2021 16:53:33 +0100 Subject: [PATCH 260/391] Return 0 for speed with relax --- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs | 3 +++ osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 4c8d0b2ce6..651fd056f0 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -37,6 +37,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; double flashlightRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier; + if (mods.Any(h => h is OsuModRelax)) + speedRating = 0.0; + 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 baseFlashlightPerformance = 0.0; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index e8169ba660..b4421d8750 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -81,9 +81,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills } } - if (Mods.Any(m => m is OsuModRelax)) - speedBonus = 0.0; - return (1 + (speedBonus - 1) * 0.75) * angleBonus * (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) From 57c069e0e1a2ec625c449c427b49b9051c21f1f6 Mon Sep 17 00:00:00 2001 From: apollo-dw <83023433+apollo-dw@users.noreply.github.com> Date: Wed, 6 Oct 2021 23:50:23 +0100 Subject: [PATCH 261/391] Remove unused strings --- osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index b4421d8750..9364b11048 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -7,8 +7,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; using osu.Framework.Utils; -using System.Linq; -using osu.Game.Rulesets.Osu.Mods; namespace osu.Game.Rulesets.Osu.Difficulty.Skills { From 5f129ae33c515adc30874c1df6cd475769e5d1a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 14:53:36 +0900 Subject: [PATCH 262/391] Remove local overridden storage of `Mods` in `Player` Not required and only causing headaches. Accessing mods should now be done via `GameplayState`. Closes #14912. --- osu.Game/Screens/Play/Player.cs | 39 ++++++++++++++------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ecc65c6bb0..e2ed71e28a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.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.Threading.Tasks; @@ -125,15 +124,11 @@ namespace osu.Game.Screens.Play public DimmableStoryboard DimmableStoryboard { get; private set; } - [Cached] - [Cached(Type = typeof(IBindable>))] - protected new readonly Bindable> Mods = new Bindable>(Array.Empty()); - /// /// Whether failing should be allowed. /// By default, this checks whether all selected mods allow failing. /// - protected virtual bool CheckModsAllowFailure() => Mods.Value.OfType().All(m => m.PerformFail()); + protected virtual bool CheckModsAllowFailure() => GameplayState.Mods.OfType().All(m => m.PerformFail()); public readonly PlayerConfiguration Configuration; @@ -179,12 +174,12 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader(true)] private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game) { - Mods.Value = base.Mods.Value.Select(m => m.DeepClone()).ToArray(); + var gameplayMods = Mods.Value.Select(m => m.DeepClone()).ToArray(); if (Beatmap.Value is DummyWorkingBeatmap) return; - IBeatmap playableBeatmap = loadPlayableBeatmap(); + IBeatmap playableBeatmap = loadPlayableBeatmap(gameplayMods); if (playableBeatmap == null) return; @@ -199,12 +194,12 @@ namespace osu.Game.Screens.Play if (game is OsuGame osuGame) LocalUserPlaying.BindTo(osuGame.LocalUserPlaying); - DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); + DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, gameplayMods); dependencies.CacheAs(DrawableRuleset); ScoreProcessor = ruleset.CreateScoreProcessor(); ScoreProcessor.ApplyBeatmap(playableBeatmap); - ScoreProcessor.Mods.BindTo(Mods); + ScoreProcessor.Mods.Value = gameplayMods; dependencies.CacheAs(ScoreProcessor); @@ -223,9 +218,9 @@ namespace osu.Game.Screens.Play // ensure the score is in a consistent state with the current player. Score.ScoreInfo.BeatmapInfo = Beatmap.Value.BeatmapInfo; Score.ScoreInfo.Ruleset = ruleset.RulesetInfo; - Score.ScoreInfo.Mods = Mods.Value.ToArray(); + Score.ScoreInfo.Mods = gameplayMods; - dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, Mods.Value, Score)); + dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score)); AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); @@ -302,13 +297,13 @@ namespace osu.Game.Screens.Play // this is required for mods that apply transforms to these processors. ScoreProcessor.OnLoadComplete += _ => { - foreach (var mod in Mods.Value.OfType()) + foreach (var mod in gameplayMods.OfType()) mod.ApplyToScoreProcessor(ScoreProcessor); }; HealthProcessor.OnLoadComplete += _ => { - foreach (var mod in Mods.Value.OfType()) + foreach (var mod in gameplayMods.OfType()) mod.ApplyToHealthProcessor(HealthProcessor); }; @@ -356,7 +351,7 @@ 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, Mods.Value) + HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods) { HoldToQuit = { @@ -467,7 +462,7 @@ namespace osu.Game.Screens.Play } } - private IBeatmap loadPlayableBeatmap() + private IBeatmap loadPlayableBeatmap(Mod[] gameplayMods) { IBeatmap playable; @@ -481,7 +476,7 @@ namespace osu.Game.Screens.Play try { - playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Mods.Value); + playable = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, gameplayMods); } catch (BeatmapInvalidForRulesetException) { @@ -489,7 +484,7 @@ namespace osu.Game.Screens.Play rulesetInfo = Beatmap.Value.BeatmapInfo.Ruleset; ruleset = rulesetInfo.CreateInstance(); - playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, Mods.Value); + playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, gameplayMods); } if (playable.HitObjects.Count == 0) @@ -789,7 +784,7 @@ namespace osu.Game.Screens.Play failAnimation.Start(); - if (Mods.Value.OfType().Any(m => m.RestartOnFail)) + if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) Restart(); return true; @@ -919,17 +914,17 @@ namespace osu.Game.Screens.Play storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable; - foreach (var mod in Mods.Value.OfType()) + foreach (var mod in GameplayState.Mods.OfType()) mod.ApplyToPlayer(this); - foreach (var mod in Mods.Value.OfType()) + foreach (var mod in GameplayState.Mods.OfType()) mod.ApplyToHUD(HUDOverlay); // Our mods are local copies of the global mods so they need to be re-applied to the track. // This is done through the music controller (for now), because resetting speed adjustments on the beatmap track also removes adjustments provided by DrawableTrack. // Todo: In the future, player will receive in a track and will probably not have to worry about this... musicController.ResetTrackAdjustments(); - foreach (var mod in Mods.Value.OfType()) + foreach (var mod in GameplayState.Mods.OfType()) mod.ApplyToTrack(musicController.CurrentTrack); updateGameplayState(); From 697f53c4450882f3c3e70262a4c167422e9921a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 15:00:37 +0900 Subject: [PATCH 263/391] Fix test failure due to reference of `Player.Mods` --- osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 8160a62991..aee15a145c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -205,7 +205,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen()); AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre)); AddUntilStep("wait for player to be current", () => player.IsCurrentScreen()); - AddStep("retrieve mods", () => playerMod1 = (TestMod)player.Mods.Value.Single()); + AddStep("retrieve mods", () => playerMod1 = (TestMod)player.GameplayState.Mods.Single()); AddAssert("game mods not applied", () => gameMod.Applied == false); AddAssert("player mods applied", () => playerMod1.Applied); @@ -217,7 +217,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddUntilStep("wait for player to be current", () => player.IsCurrentScreen()); - AddStep("retrieve mods", () => playerMod2 = (TestMod)player.Mods.Value.Single()); + AddStep("retrieve mods", () => playerMod2 = (TestMod)player.GameplayState.Mods.Single()); AddAssert("game mods not applied", () => gameMod.Applied == false); AddAssert("player has different mods", () => playerMod1 != playerMod2); AddAssert("player mods applied", () => playerMod2.Applied); From a57b080f10050c8c9630def0fdcbf99f1718e97a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 15:27:57 +0900 Subject: [PATCH 264/391] Avoid showing the disclaimer in game tests No real performance gain, but this is handy to bypass when actually using one of these tests to test something. --- osu.Game/Tests/Visual/OsuGameTestScene.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index c025cf85c7..dba73b0024 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -126,6 +126,8 @@ namespace osu.Game.Tests.Visual public new Bindable> SelectedMods => base.SelectedMods; + public override Version AssemblyVersion => new Version(0, 0); + // if we don't do this, when running under nUnit the version that gets populated is that of nUnit. public override string Version => "test game"; From 290c9755e261d2732b85f305feb9ba6b0a5535ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 15:46:50 +0900 Subject: [PATCH 265/391] Always use circles intro for `OsuGame` tests The triangles intro tracks video time, which is not adjusted based on the game's playback rate (ie. it runs in realtime even for headless tests). Maybe we want to make the triangles video adjust its rate along with tests? --- osu.Game/Tests/Visual/OsuGameTestScene.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index dba73b0024..94715dfc1a 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -22,6 +22,7 @@ using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Menu; using osuTK.Graphics; +using IntroSequence = osu.Game.Configuration.IntroSequence; namespace osu.Game.Tests.Visual { @@ -144,6 +145,9 @@ namespace osu.Game.Tests.Visual protected override void LoadComplete() { base.LoadComplete(); + + LocalConfig.SetValue(OsuSetting.IntroSequence, IntroSequence.Circles); + API.Login("Rhythm Champion", "osu!"); Dependencies.Get().SetValue(Static.MutedAudioNotificationShownOnce, true); From 0bd5136a293c99e31a3ead1af16f989027666b82 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 15:47:59 +0900 Subject: [PATCH 266/391] Fix `TestOverlayClosing` occasionally failing due to running too fast --- .../Visual/Navigation/TestSceneScreenNavigation.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index aeb800f58a..ce437e7299 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -350,13 +350,13 @@ namespace osu.Game.Tests.Visual.Navigation // since most overlays use a scroll container that absorbs on mouse down NowPlayingOverlay nowPlayingOverlay = null; - AddStep("enter menu", () => InputManager.Key(Key.Enter)); + AddUntilStep("Wait for now playing load", () => (nowPlayingOverlay = Game.ChildrenOfType().FirstOrDefault()) != null); - AddStep("get and press now playing hotkey", () => - { - nowPlayingOverlay = Game.ChildrenOfType().Single(); - InputManager.Key(Key.F6); - }); + AddStep("enter menu", () => InputManager.Key(Key.Enter)); + AddUntilStep("toolbar displayed", () => Game.Toolbar.State.Value == Visibility.Visible); + + AddStep("open now playing", () => InputManager.Key(Key.F6)); + AddUntilStep("now playing is visible", () => nowPlayingOverlay.State.Value == Visibility.Visible); // drag tests @@ -417,7 +417,7 @@ namespace osu.Game.Tests.Visual.Navigation pushEscape(); // returns to osu! logo AddStep("Hold escape", () => InputManager.PressKey(Key.Escape)); - AddUntilStep("Wait for intro", () => Game.ScreenStack.CurrentScreen is IntroTriangles); + AddUntilStep("Wait for intro", () => Game.ScreenStack.CurrentScreen is IntroScreen); AddStep("Release escape", () => InputManager.ReleaseKey(Key.Escape)); AddUntilStep("Wait for game exit", () => Game.ScreenStack.CurrentScreen == null); AddStep("test dispose doesn't crash", () => Game.Dispose()); From 7e0379441c692c5eed9f16ef8b6e36bed6bf6549 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 16:18:47 +0900 Subject: [PATCH 267/391] Change `TestSceneOsuGame` to use `OsuGameTestScene` to avoid async disposal deadlock The original implementation was done so in a way that the nested `OsuGame` would be disposed via the async queue, causing a deadlock for 10-20s during test runs. `OsuGameTestScene` was already fixed to avoid this, so consuming it here seems like the easy fix. --- .../Visual/Navigation/TestSceneOsuGame.cs | 30 +++---------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs index 7327d4053a..353ead131e 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs @@ -34,7 +34,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.Navigation { [TestFixture] - public class TestSceneOsuGame : OsuTestScene + public class TestSceneOsuGame : OsuGameTestScene { private IReadOnlyList requiredGameDependencies => new[] { @@ -84,34 +84,12 @@ namespace osu.Game.Tests.Visual.Navigation typeof(PreviewTrackManager), }; - private OsuGame game; - [Resolved] private OsuGameBase gameBase { get; set; } [Resolved] private GameHost host { get; set; } - [SetUpSteps] - public void SetUpSteps() - { - AddStep("create game", () => - { - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, - }; - - AddGame(game = new OsuGame()); - }); - - AddUntilStep("wait for load", () => game.IsLoaded); - } - [Test] public void TestNullRulesetHandled() { @@ -127,8 +105,8 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestSwitchThreadExecutionMode() { - AddStep("Change thread mode to multi threaded", () => { game.Dependencies.Get().SetValue(FrameworkSetting.ExecutionMode, ExecutionMode.MultiThreaded); }); - AddStep("Change thread mode to single thread", () => { game.Dependencies.Get().SetValue(FrameworkSetting.ExecutionMode, ExecutionMode.SingleThread); }); + AddStep("Change thread mode to multi threaded", () => { Game.Dependencies.Get().SetValue(FrameworkSetting.ExecutionMode, ExecutionMode.MultiThreaded); }); + AddStep("Change thread mode to single thread", () => { Game.Dependencies.Get().SetValue(FrameworkSetting.ExecutionMode, ExecutionMode.SingleThread); }); } [Test] @@ -154,7 +132,7 @@ namespace osu.Game.Tests.Visual.Navigation { foreach (var type in requiredGameDependencies) { - if (game.Dependencies.Get(type) == null) + if (Game.Dependencies.Get(type) == null) throw new InvalidOperationException($"{type} has not been cached"); } From c41271ea78871e4b39af13521ff93ed853552e80 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 16:26:24 +0900 Subject: [PATCH 268/391] Fix hidden test failures --- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs index af64be78f8..ed9da36b05 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4; private bool objectWithIncreasedVisibilityHasIndex(int index) - => Player.Mods.Value.OfType().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index]; + => Player.GameplayState.Mods.OfType().Single().FirstObject == Player.GameplayState.Beatmap.HitObjects[index]; private class TestOsuModHidden : OsuModHidden { From f88d8989605d2cd54e4af6ed26f5b096db88c386 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 16:38:22 +0900 Subject: [PATCH 269/391] Allow intro screens to be created without loading a `MainMenu` --- osu.Game/Screens/Loader.cs | 8 +++++--- osu.Game/Screens/Menu/IntroCircles.cs | 7 +++++++ osu.Game/Screens/Menu/IntroScreen.cs | 25 +++++++++++++++++++++---- osu.Game/Screens/Menu/IntroTriangles.cs | 6 ++++++ osu.Game/Screens/Menu/IntroWelcome.cs | 7 +++++++ 5 files changed, 46 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index 0bfabdaa15..41097a4c74 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -49,14 +49,16 @@ namespace osu.Game.Screens switch (introSequence) { case IntroSequence.Circles: - return new IntroCircles(); + return new IntroCircles(createMainMenu); case IntroSequence.Welcome: - return new IntroWelcome(); + return new IntroWelcome(createMainMenu); default: - return new IntroTriangles(); + return new IntroTriangles(createMainMenu); } + + MainMenu createMainMenu() => new MainMenu(); } protected virtual ShaderPrecompiler CreateShaderPrecompiler() => new ShaderPrecompiler(); diff --git a/osu.Game/Screens/Menu/IntroCircles.cs b/osu.Game/Screens/Menu/IntroCircles.cs index a1b8c3a203..2792d05f75 100644 --- a/osu.Game/Screens/Menu/IntroCircles.cs +++ b/osu.Game/Screens/Menu/IntroCircles.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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -20,6 +22,11 @@ namespace osu.Game.Screens.Menu private Sample welcome; + public IntroCircles([CanBeNull] Func createNextScreen = null) + : base(createNextScreen) + { + } + [BackgroundDependencyLoader] private void load(AudioManager audio) { diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index fbd33cad67..32fb9f1d6d 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.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 System; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Utils; using osu.Framework.Screens; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.IO.Archives; @@ -55,7 +57,7 @@ namespace osu.Game.Screens.Menu private LeasedBindable beatmap; - private MainMenu mainMenu; + private OsuScreen nextScreen; [Resolved] private AudioManager audio { get; set; } @@ -63,12 +65,20 @@ namespace osu.Game.Screens.Menu [Resolved] private MusicController musicController { get; set; } + [CanBeNull] + private readonly Func createNextScreen; + /// /// Whether the is provided by osu! resources, rather than a user beatmap. /// Only valid during or after . /// protected bool UsingThemedIntro { get; private set; } + protected IntroScreen([CanBeNull] Func createNextScreen = null) + { + this.createNextScreen = createNextScreen; + } + [BackgroundDependencyLoader] private void load(OsuConfigManager config, SkinManager skinManager, BeatmapManager beatmaps, Framework.Game game) { @@ -214,14 +224,21 @@ namespace osu.Game.Screens.Menu } } - protected void PrepareMenuLoad() => LoadComponentAsync(mainMenu = new MainMenu()); + protected void PrepareMenuLoad() + { + nextScreen = createNextScreen?.Invoke(); + + if (nextScreen != null) + LoadComponentAsync(nextScreen); + } protected void LoadMenu() { beatmap.Return(); DidLoadMenu = true; - this.Push(mainMenu); + if (nextScreen != null) + this.Push(nextScreen); } } } diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index a8ca17cec1..48ced63182 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -44,6 +45,11 @@ namespace osu.Game.Screens.Menu private DecoupleableInterpolatingFramedClock decoupledClock; private TrianglesIntroSequence intro; + public IntroTriangles([CanBeNull] Func createNextScreen = null) + : base(createNextScreen) + { + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index f74043b045..639591cfef 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.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 JetBrains.Annotations; using osuTK; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -32,6 +34,11 @@ namespace osu.Game.Screens.Menu private BackgroundScreenDefault background; + public IntroWelcome([CanBeNull] Func createNextScreen = null) + : base(createNextScreen) + { + } + [BackgroundDependencyLoader] private void load(AudioManager audio) { From d0001f760d82bd1953da17b39898d5ac2a4d96a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 16:50:05 +0900 Subject: [PATCH 270/391] Group applicable comment above new addition --- osu.Game/Tests/Visual/OsuGameTestScene.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index 94715dfc1a..77db697cb6 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -127,9 +127,8 @@ namespace osu.Game.Tests.Visual public new Bindable> SelectedMods => base.SelectedMods; + // if we don't apply these changes, when running under nUnit the version that gets populated is that of nUnit. public override Version AssemblyVersion => new Version(0, 0); - - // if we don't do this, when running under nUnit the version that gets populated is that of nUnit. public override string Version => "test game"; protected override Loader CreateLoader() => new TestLoader(); From ca9c79b758f531d617c450cb9c685e604f3876e5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 7 Oct 2021 16:51:42 +0900 Subject: [PATCH 271/391] Use object initialiser (fixes CI inspection) --- .../Editor/CatchSelectionBlueprintTestScene.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs index 7d806b119e..d4c2c0f0af 100644 --- a/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Catch.Tests/Editor/CatchSelectionBlueprintTestScene.cs @@ -29,8 +29,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor protected CatchSelectionBlueprintTestScene() { - EditorBeatmap = new EditorBeatmap(new CatchBeatmap()); - EditorBeatmap.Difficulty.CircleSize = 0; + EditorBeatmap = new EditorBeatmap(new CatchBeatmap()) { Difficulty = { CircleSize = 0 } }; EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 100 From 0df409c0503654f987b29e44031f9af6a1ea27c5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 17:16:44 +0900 Subject: [PATCH 272/391] Move difficulty copy to `BeatmapModelManager.Save` --- osu.Game/Beatmaps/BeatmapModelManager.cs | 2 ++ osu.Game/Screens/Edit/Editor.cs | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 787559899a..4d698d2c92 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -192,6 +192,8 @@ namespace osu.Game.Beatmaps { var setInfo = beatmapInfo.BeatmapSet; + beatmapInfo.BaseDifficulty.CopyFrom(beatmapContent.Difficulty); + using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b8a2d3612e..2ff0101dc0 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -332,8 +332,6 @@ namespace osu.Game.Screens.Edit // no longer new after first user-triggered save. isNewBeatmap = false; - playableBeatmap.BeatmapInfo.BaseDifficulty.CopyFrom(playableBeatmap.Difficulty); - // apply any set-level metadata changes. beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet); From 7dd7d35fc84550971b3271baafc390d319e63f3c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 17:19:34 +0900 Subject: [PATCH 273/391] Remove unnecessary difficulty copy operation --- osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index 54fb287661..64f1ee4a7a 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -111,8 +111,6 @@ namespace osu.Game.Tests.Beatmaps var converterResult = new Dictionary>(); - beatmap.BeatmapInfo.BaseDifficulty = beatmap.Difficulty; - var working = new ConversionWorkingBeatmap(beatmap) { ConversionGenerated = (o, r, c) => From d3efec3c045f99b3a749a966c708796fcbc02058 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 17:22:10 +0900 Subject: [PATCH 274/391] Remove unnecessary clone operations --- osu.Game/Beatmaps/WorkingBeatmap.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index c7ccfe976a..18adecb7aa 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -119,9 +119,6 @@ namespace osu.Game.Beatmaps // Apply difficulty mods if (mods.Any(m => m is IApplicableToDifficulty)) { - converted.BeatmapInfo = converted.BeatmapInfo.Clone(); - converted.Difficulty = converted.Difficulty.Clone(); - foreach (var mod in mods.OfType()) { if (cancellationSource.IsCancellationRequested) From 185bb9c122c9a15a85c03bec91ad7f0c03e6de8b Mon Sep 17 00:00:00 2001 From: Joseph Ireland Date: Thu, 7 Oct 2021 09:30:18 +0100 Subject: [PATCH 275/391] change initial strain from 1 to 0 to allow simpler implementations --- .../CatchDifficultyCalculatorTest.cs | 4 ++-- .../OsuDifficultyCalculatorTest.cs | 6 +++--- .../TaikoDifficultyCalculatorTest.cs | 8 ++++---- osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs | 2 +- osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs index 5580358f89..2fab47f857 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs @@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Catch.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; - [TestCase(4.050601681491468d, "diffcalc-test")] + [TestCase(4.0505463516206195d, "diffcalc-test")] public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(5.169743871843191d, "diffcalc-test")] + [TestCase(5.1696411260785498d, "diffcalc-test")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new CatchModDoubleTime()); diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 19881b5c33..b0d46f40fc 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -16,12 +16,12 @@ namespace osu.Game.Rulesets.Osu.Tests protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; [TestCase(6.6634445062299665d, "diffcalc-test")] - [TestCase(1.0414203870195022d, "zero-length-sliders")] + [TestCase(1.0404303969295756d, "zero-length-sliders")] public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(8.3858089051603368d, "diffcalc-test")] - [TestCase(1.2723279173428435d, "zero-length-sliders")] + [TestCase(8.3857915525197733d, "diffcalc-test")] + [TestCase(1.2705229071231638d, "zero-length-sliders")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new OsuModDoubleTime()); diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index dd3c6b317a..0ca5e1bb08 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(2.2867022617692685d, "diffcalc-test")] - [TestCase(2.2867022617692685d, "diffcalc-test-strong")] + [TestCase(2.2593624565103561d, "diffcalc-test")] + [TestCase(2.2593624565103561d, "diffcalc-test-strong")] public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(3.1704781712282624d, "diffcalc-test")] - [TestCase(3.1704781712282624d, "diffcalc-test-strong")] + [TestCase(3.1518486708786382d, "diffcalc-test")] + [TestCase(3.1518486708786382d, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new TaikoModDoubleTime()); diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs index 73bab31e82..d8babf2f32 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainDecaySkill.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// /// The current strain level. /// - protected double CurrentStrain { get; private set; } = 1; + protected double CurrentStrain { get; private set; } protected StrainDecaySkill(Mod[] mods) : base(mods) diff --git a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs index 0880f1b08e..bbd2f079aa 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/StrainSkill.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// protected virtual int SectionLength => 400; - private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section. + private double currentSectionPeak; // We also keep track of the peak strain level in the current section. private double currentSectionEnd; From e586fee09133d56d9df22cf32f598f1ab6a5e765 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 7 Oct 2021 17:46:30 +0900 Subject: [PATCH 276/391] Remove unused usings --- osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs index 353ead131e..2706ff5ceb 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneOsuGame.cs @@ -7,11 +7,8 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Configuration; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; using osu.Framework.Platform; -using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; @@ -29,7 +26,6 @@ using osu.Game.Scoring; using osu.Game.Screens.Menu; using osu.Game.Skinning; using osu.Game.Utils; -using osuTK.Graphics; namespace osu.Game.Tests.Visual.Navigation { From ee66414e4f0119c7b36f72fb745fd833a64c9645 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 17:49:13 +0900 Subject: [PATCH 277/391] Move difficulty copy inside context retrieval --- osu.Game/Beatmaps/BeatmapModelManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 4d698d2c92..9c0fc5ef8a 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -192,8 +192,6 @@ namespace osu.Game.Beatmaps { var setInfo = beatmapInfo.BeatmapSet; - beatmapInfo.BaseDifficulty.CopyFrom(beatmapContent.Difficulty); - using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) @@ -204,6 +202,8 @@ namespace osu.Game.Beatmaps using (ContextFactory.GetForWrite()) { beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == beatmapInfo.ID); + beatmapInfo.BaseDifficulty.CopyFrom(beatmapContent.Difficulty); + var metadata = beatmapInfo.Metadata ?? setInfo.Metadata; // grab the original file (or create a new one if not found). From 5c48340520cb5b8df5ae8852f01c0b1f0239aa43 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 7 Oct 2021 14:15:16 +0900 Subject: [PATCH 278/391] Add filter effect to beatmap loading --- 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 969527a758..4168dda867 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Transforms; using osu.Framework.Input; using osu.Framework.Screens; using osu.Framework.Threading; +using osu.Game.Audio.Effects; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -63,6 +64,8 @@ namespace osu.Game.Screens.Play private readonly BindableDouble volumeAdjustment = new BindableDouble(1); + private Filter lpFilter; + protected bool BackgroundBrightnessReduction { set @@ -127,7 +130,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load(SessionStatics sessionStatics) + private void load(SessionStatics sessionStatics, AudioManager audio) { muteWarningShownOnce = sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce); batteryWarningShownOnce = sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce); @@ -159,7 +162,8 @@ namespace osu.Game.Screens.Play new InputSettings() } }, - idleTracker = new IdleTracker(750) + idleTracker = new IdleTracker(750), + lpFilter = new Filter(audio.TrackMixer) }); if (Beatmap.Value.BeatmapInfo.EpilepsyWarning) @@ -191,6 +195,7 @@ namespace osu.Game.Screens.Play epilepsyWarning.DimmableBackground = b; }); + lpFilter.CutoffTo(500, 100, Easing.OutCubic); Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); content.ScaleTo(0.7f); @@ -229,6 +234,7 @@ namespace osu.Game.Screens.Play // stop the track before removing adjustment to avoid a volume spike. Beatmap.Value.Track.Stop(); Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); + lpFilter.CutoffTo(lpFilter.MaxCutoff); } public override bool OnExiting(IScreen next) @@ -242,6 +248,7 @@ namespace osu.Game.Screens.Play BackgroundBrightnessReduction = false; Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); + lpFilter.CutoffTo(lpFilter.MaxCutoff, 100, Easing.InCubic); return base.OnExiting(next); } From cc209f0f2d71a0a27ab52cd3e1d4a1642997e5b4 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 7 Oct 2021 14:15:47 +0900 Subject: [PATCH 279/391] Add filter effect to fail sequence --- osu.Game/Screens/Play/Player.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9927467bd6..c3b733ae58 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -16,6 +16,7 @@ using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; +using osu.Game.Audio.Effects; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Containers; @@ -62,6 +63,8 @@ namespace osu.Game.Screens.Play private readonly Bindable samplePlaybackDisabled = new Bindable(); + private Filter lpFilter; + /// /// Whether gameplay should pause when the game window focus is lost. /// @@ -227,6 +230,7 @@ namespace osu.Game.Screens.Play AddInternal(GameplayBeatmap = new GameplayBeatmap(playableBeatmap)); AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); + AddInternal(lpFilter = new Filter(audio.TrackMixer)); dependencies.CacheAs(GameplayBeatmap); @@ -788,6 +792,7 @@ namespace osu.Game.Screens.Play if (PauseOverlay.State.Value == Visibility.Visible) PauseOverlay.Hide(); + lpFilter.CutoffTo(300, 2500, Easing.OutCubic); failAnimation.Start(); if (Mods.Value.OfType().Any(m => m.RestartOnFail)) @@ -800,6 +805,7 @@ namespace osu.Game.Screens.Play private void onFailComplete() { GameplayClockContainer.Stop(); + lpFilter.CutoffTo(lpFilter.MaxCutoff); FailOverlay.Retries = RestartCount; FailOverlay.Show(); From f6458aa26d9b7cba848faea8ab1a1e75ad8e88ec Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 7 Oct 2021 17:56:11 +0900 Subject: [PATCH 280/391] Add filter effect to collection management dialog --- osu.Game/Collections/ManageCollectionsDialog.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index 680fec904f..c239396a41 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -2,11 +2,13 @@ // 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 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; @@ -19,6 +21,7 @@ namespace osu.Game.Collections { private const double enter_duration = 500; private const double exit_duration = 200; + private Filter lpFilter; [Resolved(CanBeNull = true)] private CollectionManager collectionManager { get; set; } @@ -36,7 +39,7 @@ namespace osu.Game.Collections } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, AudioManager audio) { Children = new Drawable[] { @@ -108,7 +111,8 @@ namespace osu.Game.Collections }, } } - } + }, + lpFilter = new Filter(audio.TrackMixer) }; } @@ -116,6 +120,7 @@ namespace osu.Game.Collections { base.PopIn(); + lpFilter.CutoffTo(300, 100, Easing.OutCubic); this.FadeIn(enter_duration, Easing.OutQuint); this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutQuint); } @@ -124,6 +129,7 @@ namespace osu.Game.Collections { base.PopOut(); + lpFilter.CutoffTo(lpFilter.MaxCutoff, 100, Easing.InCubic); this.FadeOut(exit_duration, Easing.OutQuint); this.ScaleTo(0.9f, exit_duration); From 189358240d4e11857062381d1c6b7e1f5873a9e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 18:39:48 +0900 Subject: [PATCH 281/391] Fix intro tests and move steps out of constructor --- osu.Game.Tests/Visual/Menus/IntroTestScene.cs | 13 +++++++++---- .../Visual/Menus/TestSceneIntroCircles.cs | 3 +-- .../Visual/Menus/TestSceneIntroTriangles.cs | 3 +-- .../Visual/Menus/TestSceneIntroWelcome.cs | 3 +-- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs index f71d13ed35..6e1f789bfe 100644 --- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs +++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs @@ -5,7 +5,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; -using osu.Framework.Screens; using osu.Game.Screens; using osu.Game.Screens.Menu; using osuTK; @@ -21,6 +20,8 @@ namespace osu.Game.Tests.Visual.Menus protected OsuScreenStack IntroStack; + private IntroScreen intro; + protected IntroTestScene() { Children = new Drawable[] @@ -39,7 +40,11 @@ namespace osu.Game.Tests.Visual.Menus Position = new Vector2(0.5f), } }; + } + [Test] + public void TestPlayIntro() + { AddStep("restart sequence", () => { logo.FinishTransforms(); @@ -52,12 +57,12 @@ namespace osu.Game.Tests.Visual.Menus RelativeSizeAxes = Axes.Both, }); - IntroStack.Push(CreateScreen()); + IntroStack.Push(intro = CreateScreen()); }); - AddUntilStep("wait for menu", () => IntroStack.CurrentScreen is MainMenu); + AddUntilStep("wait for menu", () => intro.DidLoadMenu); } - protected abstract IScreen CreateScreen(); + protected abstract IntroScreen CreateScreen(); } } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs index 107734cc8d..ffc99185fb 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroCircles.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Screens; using osu.Game.Screens.Menu; namespace osu.Game.Tests.Visual.Menus @@ -10,6 +9,6 @@ namespace osu.Game.Tests.Visual.Menus [TestFixture] public class TestSceneIntroCircles : IntroTestScene { - protected override IScreen CreateScreen() => new IntroCircles(); + protected override IntroScreen CreateScreen() => new IntroCircles(); } } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs index df79584167..8f01e0321b 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroTriangles.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Screens; using osu.Game.Screens.Menu; namespace osu.Game.Tests.Visual.Menus @@ -10,6 +9,6 @@ namespace osu.Game.Tests.Visual.Menus [TestFixture] public class TestSceneIntroTriangles : IntroTestScene { - protected override IScreen CreateScreen() => new IntroTriangles(); + protected override IntroScreen CreateScreen() => new IntroTriangles(); } } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs index 5f135febf4..71681dd03e 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Screens; using osu.Framework.Utils; using osu.Game.Screens.Menu; @@ -11,7 +10,7 @@ namespace osu.Game.Tests.Visual.Menus [TestFixture] public class TestSceneIntroWelcome : IntroTestScene { - protected override IScreen CreateScreen() => new IntroWelcome(); + protected override IntroScreen CreateScreen() => new IntroWelcome(); public TestSceneIntroWelcome() { From 0348c6c7e596a54977085ebd5d603bad47507ea7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 18:47:52 +0900 Subject: [PATCH 282/391] Apply some renaming and code quality fixes --- .../Visual/Audio/TestSceneFilter.cs | 4 +-- .../Effects/{Filter.cs => AudioFilter.cs} | 29 +++++++++++++------ osu.Game/Overlays/DialogOverlay.cs | 2 +- 3 files changed, 23 insertions(+), 12 deletions(-) rename osu.Game/Audio/Effects/{Filter.cs => AudioFilter.cs} (80%) diff --git a/osu.Game.Tests/Visual/Audio/TestSceneFilter.cs b/osu.Game.Tests/Visual/Audio/TestSceneFilter.cs index c639da63d3..6e5c8e8850 100644 --- a/osu.Game.Tests/Visual/Audio/TestSceneFilter.cs +++ b/osu.Game.Tests/Visual/Audio/TestSceneFilter.cs @@ -66,10 +66,10 @@ namespace osu.Game.Tests.Visual.Audio } [Test] - public void TestLowPass() => testFilter(lowpassFilter, lowpassFilter.MaxCutoff, 0); + public void TestLowPass() => testFilter(lowpassFilter, AudioFilter.MAX_LOWPASS_CUTOFF, 0); [Test] - public void TestHighPass() => testFilter(highpassFilter, 0, highpassFilter.MaxCutoff); + public void TestHighPass() => testFilter(highpassFilter, 0, AudioFilter.MAX_LOWPASS_CUTOFF); private void testFilter(Filter filter, int cutoffFrom, int cutoffTo) { diff --git a/osu.Game/Audio/Effects/Filter.cs b/osu.Game/Audio/Effects/AudioFilter.cs similarity index 80% rename from osu.Game/Audio/Effects/Filter.cs rename to osu.Game/Audio/Effects/AudioFilter.cs index 428a69bb19..611ece5bdb 100644 --- a/osu.Game/Audio/Effects/Filter.cs +++ b/osu.Game/Audio/Effects/AudioFilter.cs @@ -11,11 +11,19 @@ namespace osu.Game.Audio.Effects { public class Filter : Component, ITransformableFilter { - public readonly int MaxCutoff = 22049; // nyquist - 1hz + /// + /// The maximum cutoff frequency that can be used with a low-pass filter. + /// This is equal to nyquist - 1hz. + /// + public const int MAX_LOWPASS_CUTOFF = 22049; // nyquist - 1hz + private readonly AudioMixer mixer; private readonly BQFParameters filter; private readonly BQFType type; + /// + /// The current cutoff of this filter. + /// public BindableNumber Cutoff { get; } /// @@ -37,7 +45,7 @@ namespace osu.Game.Audio.Effects break; case BQFType.LowPass: - initialCutoff = MaxCutoff; + initialCutoff = MAX_LOWPASS_CUTOFF; break; default: @@ -48,8 +56,9 @@ namespace osu.Game.Audio.Effects Cutoff = new BindableNumber(initialCutoff) { MinValue = 1, - MaxValue = MaxCutoff + MaxValue = MAX_LOWPASS_CUTOFF }; + filter = new BQFParameters { lFilter = type, @@ -82,13 +91,13 @@ namespace osu.Game.Audio.Effects // Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz. if (type == BQFType.LowPass) { - if (cutoff.NewValue >= MaxCutoff) + if (cutoff.NewValue >= MAX_LOWPASS_CUTOFF) { detachFilter(); return; } - if (cutoff.OldValue >= MaxCutoff && cutoff.NewValue < MaxCutoff) + if (cutoff.OldValue >= MAX_LOWPASS_CUTOFF && cutoff.NewValue < MAX_LOWPASS_CUTOFF) attachFilter(); } @@ -108,11 +117,13 @@ namespace osu.Game.Audio.Effects var filterIndex = mixer.Effects.IndexOf(filter); if (filterIndex < 0) return; - var existingFilter = mixer.Effects[filterIndex] as BQFParameters; - if (existingFilter == null) return; + if (mixer.Effects[filterIndex] is BQFParameters existingFilter) + { + existingFilter.fCenter = cutoff.NewValue; - existingFilter.fCenter = cutoff.NewValue; - mixer.Effects[filterIndex] = existingFilter; + // required to update effect with new parameters. + mixer.Effects[filterIndex] = existingFilter; + } } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index bd20b74970..5f4ab35a03 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -89,7 +89,7 @@ namespace osu.Game.Overlays { base.PopOut(); - lpFilter.CutoffTo(lpFilter.MaxCutoff, 100, Easing.InCubic); + lpFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic); if (CurrentDialog?.State.Value == Visibility.Visible) { From e578046b206dcf0d2a87da7c075fde8f90b29023 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 18:49:30 +0900 Subject: [PATCH 283/391] Rename `Filter` -> `AudioFilter` --- ...{TestSceneFilter.cs => TestSceneAudioFilter.cs} | 14 ++++++++------ osu.Game/Audio/Effects/AudioFilter.cs | 4 ++-- osu.Game/Overlays/DialogOverlay.cs | 8 ++++---- 3 files changed, 14 insertions(+), 12 deletions(-) rename osu.Game.Tests/Visual/Audio/{TestSceneFilter.cs => TestSceneAudioFilter.cs} (89%) diff --git a/osu.Game.Tests/Visual/Audio/TestSceneFilter.cs b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs similarity index 89% rename from osu.Game.Tests/Visual/Audio/TestSceneFilter.cs rename to osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs index 6e5c8e8850..cf93273ae2 100644 --- a/osu.Game.Tests/Visual/Audio/TestSceneFilter.cs +++ b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs @@ -15,13 +15,15 @@ using osu.Game.Graphics.UserInterface; namespace osu.Game.Tests.Visual.Audio { - public class TestSceneFilter : OsuTestScene + public class TestSceneAudioFilter : OsuTestScene { private WorkingBeatmap testBeatmap; + private OsuSpriteText lowpassText; + private AudioFilter highpassFilter; + + private AudioFilter lowpassFilter; private OsuSpriteText highpassText; - private Filter lowpassFilter; - private Filter highpassFilter; [BackgroundDependencyLoader] private void load(AudioManager audio) @@ -31,8 +33,8 @@ namespace osu.Game.Tests.Visual.Audio { Children = new Drawable[] { - lowpassFilter = new Filter(audio.TrackMixer), - highpassFilter = new Filter(audio.TrackMixer, BQFType.HighPass), + lowpassFilter = new AudioFilter(audio.TrackMixer), + highpassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass), lowpassText = new OsuSpriteText { Padding = new MarginPadding(20), @@ -71,7 +73,7 @@ namespace osu.Game.Tests.Visual.Audio [Test] public void TestHighPass() => testFilter(highpassFilter, 0, AudioFilter.MAX_LOWPASS_CUTOFF); - private void testFilter(Filter filter, int cutoffFrom, int cutoffTo) + private void testFilter(AudioFilter filter, int cutoffFrom, int cutoffTo) { AddStep("Load Track", () => testBeatmap.LoadTrack()); AddStep("Play Track", () => testBeatmap.Track.Start()); diff --git a/osu.Game/Audio/Effects/AudioFilter.cs b/osu.Game/Audio/Effects/AudioFilter.cs index 611ece5bdb..ee48bdd7d9 100644 --- a/osu.Game/Audio/Effects/AudioFilter.cs +++ b/osu.Game/Audio/Effects/AudioFilter.cs @@ -9,7 +9,7 @@ using osu.Framework.Graphics; namespace osu.Game.Audio.Effects { - public class Filter : Component, ITransformableFilter + public class AudioFilter : Component, ITransformableFilter { /// /// The maximum cutoff frequency that can be used with a low-pass filter. @@ -31,7 +31,7 @@ namespace osu.Game.Audio.Effects /// /// The mixer this effect should be applied to. /// The type of filter (e.g. LowPass, HighPass, etc) - public Filter(AudioMixer mixer, BQFType type = BQFType.LowPass) + public AudioFilter(AudioMixer mixer, BQFType type = BQFType.LowPass) { this.mixer = mixer; this.type = type; diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 5f4ab35a03..9db0f34d1b 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays protected override string PopInSampleName => "UI/dialog-pop-in"; protected override string PopOutSampleName => "UI/dialog-pop-out"; - private Filter lpFilter; + private AudioFilter lowPassFilter; public PopupDialog CurrentDialog { get; private set; } @@ -42,7 +42,7 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load(AudioManager audio) { - AddInternal(lpFilter = new Filter(audio.TrackMixer)); + AddInternal(lowPassFilter = new AudioFilter(audio.TrackMixer)); } public void Push(PopupDialog dialog) @@ -82,14 +82,14 @@ namespace osu.Game.Overlays { base.PopIn(); this.FadeIn(PopupDialog.ENTER_DURATION, Easing.OutQuint); - lpFilter.CutoffTo(300, 100, Easing.OutCubic); + lowPassFilter.CutoffTo(300, 100, Easing.OutCubic); } protected override void PopOut() { base.PopOut(); - lpFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic); + lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic); if (CurrentDialog?.State.Value == Visibility.Visible) { From b88d4f19137012aafe01370b1ef6a2ba529dee02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 19:58:34 +0900 Subject: [PATCH 284/391] Fix weird edge case in `TestSceneIntroWelcome` --- osu.Game.Tests/Visual/Menus/IntroTestScene.cs | 2 +- osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs index 6e1f789bfe..bfea97410a 100644 --- a/osu.Game.Tests/Visual/Menus/IntroTestScene.cs +++ b/osu.Game.Tests/Visual/Menus/IntroTestScene.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Menus } [Test] - public void TestPlayIntro() + public virtual void TestPlayIntro() { AddStep("restart sequence", () => { diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs index 71681dd03e..9081be3dd6 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs @@ -12,8 +12,10 @@ namespace osu.Game.Tests.Visual.Menus { protected override IntroScreen CreateScreen() => new IntroWelcome(); - public TestSceneIntroWelcome() + public override void TestPlayIntro() { + base.TestPlayIntro(); + AddUntilStep("wait for load", () => MusicController.TrackLoaded); AddAssert("correct track", () => Precision.AlmostEquals(MusicController.CurrentTrack.Length, 48000, 1)); AddAssert("check if menu music loops", () => MusicController.CurrentTrack.Looping); From ba84da6ef89892658677a7f67b50b8e1e43df0b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 20:08:02 +0900 Subject: [PATCH 285/391] Tidy up test scene --- .../Visual/Audio/TestSceneAudioFilter.cs | 77 +++++++++++++------ 1 file changed, 54 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs index cf93273ae2..211543a881 100644 --- a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs +++ b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs @@ -5,11 +5,12 @@ using ManagedBass.Fx; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; using osu.Game.Audio.Effects; -using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -17,18 +18,22 @@ namespace osu.Game.Tests.Visual.Audio { public class TestSceneAudioFilter : OsuTestScene { - private WorkingBeatmap testBeatmap; - private OsuSpriteText lowpassText; + private AudioFilter lowpassFilter; + + private OsuSpriteText highpassText; private AudioFilter highpassFilter; - private AudioFilter lowpassFilter; - private OsuSpriteText highpassText; + private Track track; + + private WaveformTestBeatmap beatmap; [BackgroundDependencyLoader] private void load(AudioManager audio) { - testBeatmap = new WaveformTestBeatmap(audio); + beatmap = new WaveformTestBeatmap(audio); + track = beatmap.LoadTrack(); + Add(new FillFlowContainer { Children = new Drawable[] @@ -67,30 +72,56 @@ namespace osu.Game.Tests.Visual.Audio highpassFilter.Cutoff.ValueChanged += e => highpassText.Text = $"High Pass: {e.NewValue}hz"; } - [Test] - public void TestLowPass() => testFilter(lowpassFilter, AudioFilter.MAX_LOWPASS_CUTOFF, 0); - - [Test] - public void TestHighPass() => testFilter(highpassFilter, 0, AudioFilter.MAX_LOWPASS_CUTOFF); - - private void testFilter(AudioFilter filter, int cutoffFrom, int cutoffTo) + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Play Track", () => track.Start()); + waitTrackPlay(); + } + + [Test] + public void TestLowPass() { - AddStep("Load Track", () => testBeatmap.LoadTrack()); - AddStep("Play Track", () => testBeatmap.Track.Start()); - AddWaitStep("Let track play", 10); AddStep("Filter Sweep", () => { - filter.CutoffTo(cutoffFrom).Then() - .CutoffTo(cutoffTo, 2000, cutoffFrom > cutoffTo ? Easing.OutCubic : Easing.InCubic); + lowpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then() + .CutoffTo(0, 2000, Easing.OutCubic); }); - AddWaitStep("Let track play", 10); + + waitTrackPlay(); + AddStep("Filter Sweep (reverse)", () => { - filter.CutoffTo(cutoffTo).Then() - .CutoffTo(cutoffFrom, 2000, cutoffTo > cutoffFrom ? Easing.OutCubic : Easing.InCubic); + lowpassFilter.CutoffTo(0).Then() + .CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic); }); - AddWaitStep("Let track play", 10); - AddStep("Stop track", () => testBeatmap.Track.Stop()); + + waitTrackPlay(); + AddStep("Stop track", () => track.Stop()); } + + [Test] + public void TestHighPass() + { + AddStep("Filter Sweep", () => + { + highpassFilter.CutoffTo(0).Then() + .CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic); + }); + + waitTrackPlay(); + + AddStep("Filter Sweep (reverse)", () => + { + highpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then() + .CutoffTo(0, 2000, Easing.OutCubic); + }); + + waitTrackPlay(); + + AddStep("Stop track", () => track.Stop()); + } + + private void waitTrackPlay() => AddWaitStep("Let track play", 10); } } From 9bf29503fb94a0726c65d07b0f649e2c72aa9a97 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 20:57:14 +0900 Subject: [PATCH 286/391] Rename remaining usage --- osu.Game/Collections/ManageCollectionsDialog.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index c239396a41..95fbfa0f86 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -21,7 +21,8 @@ namespace osu.Game.Collections { private const double enter_duration = 500; private const double exit_duration = 200; - private Filter lpFilter; + + private AudioFilter lowPassFilter; [Resolved(CanBeNull = true)] private CollectionManager collectionManager { get; set; } @@ -112,7 +113,7 @@ namespace osu.Game.Collections } } }, - lpFilter = new Filter(audio.TrackMixer) + lowPassFilter = new AudioFilter(audio.TrackMixer) }; } @@ -120,7 +121,7 @@ namespace osu.Game.Collections { base.PopIn(); - lpFilter.CutoffTo(300, 100, Easing.OutCubic); + lowPassFilter.CutoffTo(300, 100, Easing.OutCubic); this.FadeIn(enter_duration, Easing.OutQuint); this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutQuint); } @@ -129,7 +130,8 @@ namespace osu.Game.Collections { base.PopOut(); - lpFilter.CutoffTo(lpFilter.MaxCutoff, 100, Easing.InCubic); + lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic); + this.FadeOut(exit_duration, Easing.OutQuint); this.ScaleTo(0.9f, exit_duration); From da96cc73d8b3005e28b444b0afc69eac652da446 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 23:40:47 +0900 Subject: [PATCH 287/391] Fix dual specification of `SuspensionHandler` and move fields around slightly --- osu.Game/Screens/Play/Player.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 620b026060..30ba2bfe6d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -62,8 +62,6 @@ namespace osu.Game.Screens.Play private readonly Bindable samplePlaybackDisabled = new Bindable(); - private AudioFilter lowPassFilter; - /// /// Whether gameplay should pause when the game window focus is lost. /// @@ -217,7 +215,7 @@ namespace osu.Game.Screens.Play InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); - AddInternal(lowPassFilter = new AudioFilter(audio.TrackMixer)); + AddInternal(failLowPassFilter = new AudioFilter(audio.TrackMixer)); Score = CreateScore(playableBeatmap); @@ -228,8 +226,6 @@ namespace osu.Game.Screens.Play dependencies.CacheAs(GameplayState = new GameplayState(playableBeatmap, ruleset, gameplayMods, Score)); - AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); - var rulesetSkinProvider = new RulesetSkinProvidingContainer(ruleset, playableBeatmap, Beatmap.Value.Skin); // load the skinning hierarchy first. @@ -774,6 +770,8 @@ namespace osu.Game.Screens.Play private FailAnimation failAnimation; + private AudioFilter failLowPassFilter; + private bool onFail() { if (!CheckModsAllowFailure()) @@ -788,7 +786,7 @@ namespace osu.Game.Screens.Play if (PauseOverlay.State.Value == Visibility.Visible) PauseOverlay.Hide(); - lowPassFilter.CutoffTo(300, 2500, Easing.OutCubic); + failLowPassFilter.CutoffTo(300, 2500, Easing.OutCubic); failAnimation.Start(); if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) @@ -801,7 +799,7 @@ namespace osu.Game.Screens.Play private void onFailComplete() { GameplayClockContainer.Stop(); - lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF); + failLowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF); FailOverlay.Retries = RestartCount; FailOverlay.Show(); From 436ead421a71e0ca941d7e0f20aefd4f25fc83b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Oct 2021 12:27:04 +0900 Subject: [PATCH 288/391] Move low pass fail effect to `FailAnimation` --- osu.Game/Screens/Play/FailAnimation.cs | 10 +++++++++- osu.Game/Screens/Play/Player.cs | 6 ------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index 71bea2a145..e250791b72 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -10,7 +10,9 @@ using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Utils; +using osu.Game.Audio.Effects; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects.Drawables; using osuTK; @@ -22,7 +24,7 @@ namespace osu.Game.Screens.Play /// Manage the animation to be applied when a player fails. /// Single file; automatically disposed after use. /// - public class FailAnimation : Component + public class FailAnimation : CompositeDrawable { public Action OnComplete; @@ -32,6 +34,8 @@ namespace osu.Game.Screens.Play private Track track; + private AudioFilter failLowPassFilter; + private const float duration = 2500; private Sample failSample; @@ -46,6 +50,8 @@ namespace osu.Game.Screens.Play { track = beatmap.Value.Track; failSample = audio.Samples.Get(@"Gameplay/failsound"); + + AddInternal(failLowPassFilter = new AudioFilter(audio.TrackMixer)); } private bool started; @@ -68,6 +74,8 @@ namespace osu.Game.Screens.Play Expire(); }); + failLowPassFilter.CutoffTo(300, duration, Easing.OutCubic); + track.AddAdjustment(AdjustableProperty.Frequency, trackFreq); applyToPlayfield(drawableRuleset.Playfield); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 30ba2bfe6d..090210e611 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -15,7 +15,6 @@ using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Framework.Threading; -using osu.Game.Audio.Effects; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Containers; @@ -215,7 +214,6 @@ namespace osu.Game.Screens.Play InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); - AddInternal(failLowPassFilter = new AudioFilter(audio.TrackMixer)); Score = CreateScore(playableBeatmap); @@ -770,8 +768,6 @@ namespace osu.Game.Screens.Play private FailAnimation failAnimation; - private AudioFilter failLowPassFilter; - private bool onFail() { if (!CheckModsAllowFailure()) @@ -786,7 +782,6 @@ namespace osu.Game.Screens.Play if (PauseOverlay.State.Value == Visibility.Visible) PauseOverlay.Hide(); - failLowPassFilter.CutoffTo(300, 2500, Easing.OutCubic); failAnimation.Start(); if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) @@ -799,7 +794,6 @@ namespace osu.Game.Screens.Play private void onFailComplete() { GameplayClockContainer.Stop(); - failLowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF); FailOverlay.Retries = RestartCount; FailOverlay.Show(); From 2856aef4eb17ee91f8d8f5c0a7852fae2e517870 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Oct 2021 13:51:12 +0900 Subject: [PATCH 289/391] Add exception to catch any incorrect defaults of `Bindable` --- osu.Game/Overlays/Settings/SettingsTextBox.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Overlays/Settings/SettingsTextBox.cs b/osu.Game/Overlays/Settings/SettingsTextBox.cs index d28dbf1068..68562802cf 100644 --- a/osu.Game/Overlays/Settings/SettingsTextBox.cs +++ b/osu.Game/Overlays/Settings/SettingsTextBox.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 osu.Framework.Bindables; using osu.Framework.Graphics; namespace osu.Game.Overlays.Settings @@ -13,5 +15,17 @@ namespace osu.Game.Overlays.Settings RelativeSizeAxes = Axes.X, CommitOnFocusLost = true }; + + public override Bindable Current + { + get => base.Current; + set + { + if (value.Default == null) + throw new InvalidOperationException($"Bindable settings of type {nameof(Bindable)} should have a non-null default value."); + + base.Current = value; + } + } } } From 672664dce7c88beb56328e0af53377465d04ea2a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Oct 2021 13:55:20 +0900 Subject: [PATCH 290/391] Fix all remaining cases of incorrect `Bindable` defaults --- osu.Game.Tournament/Models/SeedingResult.cs | 2 +- osu.Game.Tournament/Models/TournamentRound.cs | 4 ++-- osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs | 2 +- osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tournament/Models/SeedingResult.cs b/osu.Game.Tournament/Models/SeedingResult.cs index 87aaf8bf36..d37c967762 100644 --- a/osu.Game.Tournament/Models/SeedingResult.cs +++ b/osu.Game.Tournament/Models/SeedingResult.cs @@ -10,7 +10,7 @@ namespace osu.Game.Tournament.Models { public List Beatmaps = new List(); - public Bindable Mod = new Bindable(); + public Bindable Mod = new Bindable(string.Empty); public Bindable Seed = new BindableInt { diff --git a/osu.Game.Tournament/Models/TournamentRound.cs b/osu.Game.Tournament/Models/TournamentRound.cs index 08b3143be1..ab39605d07 100644 --- a/osu.Game.Tournament/Models/TournamentRound.cs +++ b/osu.Game.Tournament/Models/TournamentRound.cs @@ -14,8 +14,8 @@ namespace osu.Game.Tournament.Models [Serializable] public class TournamentRound { - public readonly Bindable Name = new Bindable(); - public readonly Bindable Description = new Bindable(); + public readonly Bindable Name = new Bindable(string.Empty); + public readonly Bindable Description = new Bindable(string.Empty); public readonly BindableInt BestOf = new BindableInt(9) { Default = 9, MinValue = 3, MaxValue = 23 }; diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs index 6e4fc8fe1a..1d8c4e7476 100644 --- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs @@ -149,7 +149,7 @@ namespace osu.Game.Tournament.Screens.Editors private readonly Bindable beatmapId = new Bindable(); - private readonly Bindable mods = new Bindable(); + private readonly Bindable mods = new Bindable(string.Empty); private readonly Container drawableContainer; diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index b64a3993e6..d5b55823a5 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -149,7 +149,7 @@ namespace osu.Game.Tournament.Screens.Editors private readonly Bindable beatmapId = new Bindable(); - private readonly Bindable score = new Bindable(); + private readonly Bindable score = new Bindable(string.Empty); private readonly Container drawableContainer; From 9dc035757fc3e1b763e227de33d442d8641381e7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Oct 2021 13:51:41 +0900 Subject: [PATCH 291/391] Fix weird textbox behaviour when entering a random mod seed overflowing int backing --- osu.Game/Overlays/Settings/SettingsNumberBox.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Settings/SettingsNumberBox.cs b/osu.Game/Overlays/Settings/SettingsNumberBox.cs index 2fbe522479..d36aa2bfc2 100644 --- a/osu.Game/Overlays/Settings/SettingsNumberBox.cs +++ b/osu.Game/Overlays/Settings/SettingsNumberBox.cs @@ -36,6 +36,7 @@ namespace osu.Game.Overlays.Settings { numberBox = new OutlinedNumberBox { + LengthLimit = 9, // limited to less than a value that could overflow int32 backing. Margin = new MarginPadding { Top = 5 }, RelativeSizeAxes = Axes.X, CommitOnFocusLost = true From b82ed3f1674520ae8680e845d873eb88d39130d5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 8 Oct 2021 14:23:53 +0900 Subject: [PATCH 292/391] Fix potential blocking operation on OrderByTotalScoreAsync() In reality this wouldn't be a long process, but the blocking is really noticeable if you add a Task.Delay(1000) in GetTotalScoreAsync(). --- osu.Game/Scoring/ScoreManager.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index dde956233b..5481eea4aa 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -72,9 +72,12 @@ namespace osu.Game.Scoring } } - // We're calling .Result, but this should not be a blocking call due to the above GetDifficultyAsync() calls. - return scores.OrderByDescending(s => GetTotalScoreAsync(s, cancellationToken: cancellationToken).Result) - .ThenBy(s => s.OnlineScoreID) + var totalScores = await Task.WhenAll(scores.Select(s => GetTotalScoreAsync(s, cancellationToken: cancellationToken))).ConfigureAwait(false); + + return scores.Select((s, i) => (index: i, score: s)) + .OrderByDescending(key => totalScores[key.index]) + .ThenBy(key => key.score.OnlineScoreID) + .Select(key => key.score) .ToArray(); } From d6ac6a5cd60263ae778ebb488541e1a28a65a1c8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 8 Oct 2021 15:18:01 +0900 Subject: [PATCH 293/391] Fix intermittent results screen test failures --- .../TestScenePlaylistsResultsScreen.cs | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 4bcc887b9f..d948aebbbf 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -32,12 +32,14 @@ namespace osu.Game.Tests.Visual.Playlists private TestResultsScreen resultsScreen; private int currentScoreId; private bool requestComplete; + private int totalCount; [SetUp] public void Setup() => Schedule(() => { currentScoreId = 0; requestComplete = false; + totalCount = 0; bindHandler(); }); @@ -53,7 +55,6 @@ namespace osu.Game.Tests.Visual.Playlists }); createResults(() => userScore); - waitForDisplay(); AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded); } @@ -62,7 +63,6 @@ namespace osu.Game.Tests.Visual.Playlists public void TestShowNullUserScore() { createResults(); - waitForDisplay(); AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); } @@ -79,7 +79,6 @@ namespace osu.Game.Tests.Visual.Playlists }); createResults(() => userScore); - waitForDisplay(); AddAssert("more than 1 panel displayed", () => this.ChildrenOfType().Count() > 1); AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded); @@ -91,7 +90,6 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind delayed handler", () => bindHandler(true)); createResults(); - waitForDisplay(); AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); } @@ -100,7 +98,6 @@ namespace osu.Game.Tests.Visual.Playlists public void TestFetchWhenScrolledToTheRight() { createResults(); - waitForDisplay(); AddStep("bind delayed handler", () => bindHandler(true)); @@ -131,7 +128,6 @@ namespace osu.Game.Tests.Visual.Playlists }); createResults(() => userScore); - waitForDisplay(); AddStep("bind delayed handler", () => bindHandler(true)); @@ -161,13 +157,15 @@ namespace osu.Game.Tests.Visual.Playlists })); }); - AddUntilStep("wait for load", () => resultsScreen.ChildrenOfType().FirstOrDefault()?.AllPanelsVisible == true); + waitForDisplay(); } private void waitForDisplay() { - AddUntilStep("wait for request to complete", () => requestComplete); - AddUntilStep("wait for panels to be visible", () => resultsScreen.ChildrenOfType().FirstOrDefault()?.AllPanelsVisible == true); + AddUntilStep("wait for load to complete", () => + requestComplete + && resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount + && resultsScreen.ScorePanelList.AllPanelsVisible); AddWaitStep("wait for display", 5); } @@ -203,6 +201,7 @@ namespace osu.Game.Tests.Visual.Playlists triggerFail(s); else triggerSuccess(s, createUserResponse(userScore)); + break; case IndexPlaylistScoresRequest i: @@ -248,6 +247,8 @@ namespace osu.Game.Tests.Visual.Playlists } }; + totalCount++; + for (int i = 1; i <= scores_per_result; i++) { multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore @@ -285,6 +286,8 @@ namespace osu.Game.Tests.Visual.Playlists }, Statistics = userScore.Statistics }); + + totalCount += 2; } addCursor(multiplayerUserScore.ScoresAround.Lower); @@ -325,6 +328,8 @@ namespace osu.Game.Tests.Visual.Playlists { HitResult.Great, 300 } } }); + + totalCount++; } addCursor(result); From f199d6c5212c360fb9c2c16b70d21d3e9730c7dd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 8 Oct 2021 15:26:25 +0900 Subject: [PATCH 294/391] Fix another related test failure --- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 189b143a35..9a75d3c309 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -163,7 +163,6 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddUntilStep("wait for fetch", () => leaderboard.Scores != null); - AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID)); } @@ -171,6 +170,7 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestDeleteViaDatabase() { AddStep("delete top score", () => scoreManager.Delete(importedScores[0])); + AddUntilStep("wait for fetch", () => leaderboard.Scores != null); AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != importedScores[0].OnlineScoreID)); } } From 95c67a594bbb693d1a5eccdba22ae2dcf425eeaa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 8 Oct 2021 18:23:18 +0900 Subject: [PATCH 295/391] Fix tests --- .../OsuDifficultyCalculatorTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 19881b5c33..15675e74d1 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; - [TestCase(6.6634445062299665d, "diffcalc-test")] - [TestCase(1.0414203870195022d, "zero-length-sliders")] + [TestCase(6.5867229481955389d, "diffcalc-test")] + [TestCase(1.0416315570967911d, "zero-length-sliders")] public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(8.3858089051603368d, "diffcalc-test")] - [TestCase(1.2723279173428435d, "zero-length-sliders")] + [TestCase(8.2730989071947896d, "diffcalc-test")] + [TestCase(1.2726413186221039d, "zero-length-sliders")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new OsuModDoubleTime()); From f0affa9f5af37796d822c0e8c9b6ad408ebe0636 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 8 Oct 2021 19:50:31 +0900 Subject: [PATCH 296/391] Don't refer to BeatmapInfo --- 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 c236b3750b..b1c628d273 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; - double drainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate; + double drainRate = beatmap.Difficulty.DrainRate; int maxCombo = beatmap.HitObjects.Count; // Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above) From 06a78d9729a17f119c18f455b9a1a3e30342e8e8 Mon Sep 17 00:00:00 2001 From: Joseph Ireland Date: Sat, 9 Oct 2021 00:15:27 +0100 Subject: [PATCH 297/391] fix taiko tests --- .../TaikoDifficultyCalculatorTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index 0ca5e1bb08..4b0b74ad27 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(2.2593624565103561d, "diffcalc-test")] - [TestCase(2.2593624565103561d, "diffcalc-test-strong")] + [TestCase(2.2420075288523802d, "diffcalc-test")] + [TestCase(2.2420075288523802d, "diffcalc-test-strong")] public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(3.1518486708786382d, "diffcalc-test")] - [TestCase(3.1518486708786382d, "diffcalc-test-strong")] + [TestCase(3.134084469440479d, "diffcalc-test")] + [TestCase(3.134084469440479d, "diffcalc-test-strong")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new TaikoModDoubleTime()); From 37632fe4dcacf2ea839005b33ac62017dfc21502 Mon Sep 17 00:00:00 2001 From: StanR Date: Sat, 9 Oct 2021 12:08:57 +0300 Subject: [PATCH 298/391] Remove int casts in difficulty calculation, fixup some comments --- .../Difficulty/OsuDifficultyCalculator.cs | 5 ++--- .../Difficulty/OsuPerformanceCalculator.cs | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index b1c628d273..790aa0eb7d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; - double preempt = (int)IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; + double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double drainRate = beatmap.Difficulty.DrainRate; int maxCombo = beatmap.HitObjects.Count; @@ -102,8 +102,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty HitWindows hitWindows = new OsuHitWindows(); hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty); - // Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future - hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate; + hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate; return new Skill[] { diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index f868b5e1ce..4e4dbc02a1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -40,9 +40,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); - // Custom multipliers for NoFail and SpunOut. double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. + // Custom multipliers for NoFail and SpunOut. if (mods.Any(m => m is OsuModNoFail)) multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss); @@ -114,11 +114,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor; - // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. if (mods.Any(m => m is OsuModBlinds)) aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * countMiss)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate); else if (mods.Any(h => h is OsuModHidden)) + { + // 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); + } aimValue *= approachRateBonus; @@ -155,14 +157,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor; - // Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given. if (mods.Any(m => m is OsuModBlinds)) + { + // Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given. speedValue *= 1.12; + } else if (mods.Any(m => m is OsuModHidden)) + { + // 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); + } // Scale the speed value with accuracy and OD. speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2); + // Scale the speed value with # of 50s to punish doubletapping. speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); @@ -199,6 +207,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty accuracyValue *= 1.14; else if (mods.Any(m => m is OsuModHidden)) accuracyValue *= 1.08; + if (mods.Any(m => m is OsuModFlashlight)) accuracyValue *= 1.02; From 8007ba93eb31b86808cdd91bb985b815793aa088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 9 Oct 2021 14:25:40 +0200 Subject: [PATCH 299/391] Fix typo in `TaikoMultiplierAppliedDifficulty` class name --- .../Beatmaps/TaikoBeatmapConverter.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index ef8cf5cb53..50c0ca7f55 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -47,10 +47,10 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) { - if (!(original.Difficulty is TaikoMutliplierAppliedDifficulty)) + if (!(original.Difficulty is TaikoMultiplierAppliedDifficulty)) { // Rewrite the beatmap info to add the slider velocity multiplier - original.Difficulty = new TaikoMutliplierAppliedDifficulty(original.Difficulty); + original.Difficulty = new TaikoMultiplierAppliedDifficulty(original.Difficulty); } Beatmap converted = base.ConvertBeatmap(original, cancellationToken); @@ -191,15 +191,15 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps protected override Beatmap CreateBeatmap() => new TaikoBeatmap(); - private class TaikoMutliplierAppliedDifficulty : BeatmapDifficulty + private class TaikoMultiplierAppliedDifficulty : BeatmapDifficulty { - public TaikoMutliplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty) + public TaikoMultiplierAppliedDifficulty(IBeatmapDifficultyInfo difficulty) { CopyFrom(difficulty); } [UsedImplicitly] - public TaikoMutliplierAppliedDifficulty() + public TaikoMultiplierAppliedDifficulty() { } @@ -208,14 +208,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps public override void CopyTo(BeatmapDifficulty other) { base.CopyTo(other); - if (!(other is TaikoMutliplierAppliedDifficulty)) + if (!(other is TaikoMultiplierAppliedDifficulty)) SliderMultiplier /= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; } public override void CopyFrom(IBeatmapDifficultyInfo other) { base.CopyFrom(other); - if (!(other is TaikoMutliplierAppliedDifficulty)) + if (!(other is TaikoMultiplierAppliedDifficulty)) SliderMultiplier *= LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; } From 237c06301507da05454283ff3bbd492a713f9e21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 9 Oct 2021 14:26:32 +0200 Subject: [PATCH 300/391] Fix typo in `multipleIncrementMods` variable name --- .../Visual/UserInterface/TestSceneFooterButtonMods.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs index 546e905ded..8d1572769f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs @@ -36,9 +36,9 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep(@"Add DoubleTime", () => changeMods(doubleTimeMod)); AddAssert(@"Check DoubleTime multiplier", () => assertModsMultiplier(doubleTimeMod)); - var mutlipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() }; - AddStep(@"Add multiple Mods", () => changeMods(mutlipleIncrementMods)); - AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(mutlipleIncrementMods)); + var multipleIncrementMods = new Mod[] { new OsuModDoubleTime(), new OsuModHidden(), new OsuModHardRock() }; + AddStep(@"Add multiple Mods", () => changeMods(multipleIncrementMods)); + AddAssert(@"Check multiple mod multiplier", () => assertModsMultiplier(multipleIncrementMods)); } [Test] From f6df93f0139af65cf3cd5d87603fc8fcf31f7524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 3 Oct 2021 17:30:22 +0200 Subject: [PATCH 301/391] Introduce basic parts of colour scheme to settings sidebar --- osu.Game/Overlays/Settings/SettingsSection.cs | 7 +- osu.Game/Overlays/Settings/Sidebar.cs | 14 +++- osu.Game/Overlays/Settings/SidebarButton.cs | 78 +++++++++++++------ osu.Game/Overlays/SettingsPanel.cs | 12 +-- 4 files changed, 76 insertions(+), 35 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsSection.cs b/osu.Game/Overlays/Settings/SettingsSection.cs index 2a6f3f5ed7..33c0e9cafa 100644 --- a/osu.Game/Overlays/Settings/SettingsSection.cs +++ b/osu.Game/Overlays/Settings/SettingsSection.cs @@ -12,7 +12,6 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osuTK.Graphics; namespace osu.Game.Overlays.Settings { @@ -33,7 +32,7 @@ namespace osu.Game.Overlays.Settings private const int header_size = 26; private const int margin = 20; - private const int border_size = 2; + private const int border_size = 4; public bool MatchingFilter { @@ -63,14 +62,14 @@ namespace osu.Game.Overlays.Settings } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OverlayColourProvider colourProvider, OsuColour colours) { AddRangeInternal(new Drawable[] { new Box { Name = "separator", - Colour = new Color4(0, 0, 0, 255), + Colour = colourProvider.Background6, RelativeSizeAxes = Axes.X, Height = border_size, }, diff --git a/osu.Game/Overlays/Settings/Sidebar.cs b/osu.Game/Overlays/Settings/Sidebar.cs index 4ca6e2ec42..49585b3afb 100644 --- a/osu.Game/Overlays/Settings/Sidebar.cs +++ b/osu.Game/Overlays/Settings/Sidebar.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -17,8 +18,9 @@ namespace osu.Game.Overlays.Settings { public class Sidebar : Container, IStateful { + private readonly Box background; private readonly FillFlowContainer content; - public const float DEFAULT_WIDTH = Toolbar.Toolbar.HEIGHT * 1.4f; + public const float DEFAULT_WIDTH = 70; public const int EXPANDED_WIDTH = 200; public event Action StateChanged; @@ -30,7 +32,7 @@ namespace osu.Game.Overlays.Settings RelativeSizeAxes = Axes.Y; InternalChildren = new Drawable[] { - new Box + background = new Box { Colour = OsuColour.Gray(0.02f), RelativeSizeAxes = Axes.Both, @@ -52,6 +54,12 @@ namespace osu.Game.Overlays.Settings }; } + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + background.Colour = colourProvider.Background5; + } + private ScheduledDelegate expandEvent; private ExpandedState state; @@ -80,8 +88,6 @@ namespace osu.Game.Overlays.Settings { public SidebarScrollContainer() { - Content.Anchor = Anchor.CentreLeft; - Content.Origin = Anchor.CentreLeft; RelativeSizeAxes = Axes.Both; ScrollbarVisible = false; } diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs index cf6a313a1f..a69b163998 100644 --- a/osu.Game/Overlays/Settings/SidebarButton.cs +++ b/osu.Game/Overlays/Settings/SidebarButton.cs @@ -2,12 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using osuTK; -using osuTK.Graphics; 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.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -17,11 +17,16 @@ namespace osu.Game.Overlays.Settings { public class SidebarButton : OsuButton { + private const double fade_duration = 50; + private readonly ConstrainedIconContainer iconContainer; private readonly SpriteText headerText; - private readonly Box selectionIndicator; + private readonly CircularContainer selectionIndicator; private readonly Container text; + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + // always consider as part of flow, even when not visible (for the sake of the initial animation). public override bool IsPresent => true; @@ -47,25 +52,15 @@ namespace osu.Game.Overlays.Settings { selected = value; - if (selected) - { - selectionIndicator.FadeIn(50); - text.FadeColour(Color4.White, 50); - } - else - { - selectionIndicator.FadeOut(50); - text.FadeColour(OsuColour.Gray(0.6f), 50); - } + if (IsLoaded) + updateState(); } } public SidebarButton() { - Height = Sidebar.DEFAULT_WIDTH; RelativeSizeAxes = Axes.X; - - BackgroundColour = Color4.Black; + Height = 46; AddRange(new Drawable[] { @@ -90,21 +85,60 @@ namespace osu.Game.Overlays.Settings }, } }, - selectionIndicator = new Box + selectionIndicator = new CircularContainer { Alpha = 0, - RelativeSizeAxes = Axes.Y, - Width = 5, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, + Width = 3, + Height = 18, + Masking = true, + CornerRadius = 1.5f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding + { + Left = 9, + }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White + } }, }); } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { - selectionIndicator.Colour = colours.Yellow; + BackgroundColour = colourProvider.Background5; + selectionIndicator.Colour = colourProvider.Highlight1; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateState(); + } + + protected override bool OnHover(HoverEvent e) + { + updateState(); + return false; + } + + protected override void OnHoverLost(HoverLostEvent e) => updateState(); + + private void updateState() + { + if (Selected) + { + text.FadeColour(colourProvider.Content1, fade_duration, Easing.OutQuint); + selectionIndicator.FadeIn(fade_duration, Easing.OutQuint); + return; + } + + text.FadeColour(IsHovered ? colourProvider.Light1 : colourProvider.Light3, fade_duration, Easing.OutQuint); + selectionIndicator.FadeOut(fade_duration, Easing.OutQuint); } } } diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index bda4bb5ece..11bfb91703 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using osuTK; -using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; @@ -15,7 +14,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; 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.Overlays.Settings; @@ -64,6 +62,9 @@ namespace osu.Game.Overlays public IBindable CurrentSection = new Bindable(); + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + protected SettingsPanel(bool showSidebar) { this.showSidebar = showSidebar; @@ -89,7 +90,7 @@ namespace osu.Game.Overlays Origin = Anchor.TopRight, Scale = new Vector2(2, 1), // over-extend to the left for transitions RelativeSizeAxes = Axes.Both, - Colour = OsuColour.Gray(0.05f), + Colour = colourProvider.Background4, Alpha = 1, }, loading = new LoadingLayer @@ -292,11 +293,12 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, }; - public SettingsSectionsContainer() + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) { HeaderBackground = new Box { - Colour = Color4.Black, + Colour = colourProvider.Background4, RelativeSizeAxes = Axes.Both }; } From 315581f4c8c09a132481eb2322d68388c008b03d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 9 Oct 2021 19:18:29 +0200 Subject: [PATCH 302/391] Adjust horizontal spacing in settings panel --- osu.Game/Overlays/SettingsPanel.cs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 11bfb91703..7c43875bef 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -23,7 +23,7 @@ namespace osu.Game.Overlays [Cached] public abstract class SettingsPanel : OsuFocusedOverlayContainer { - public const float CONTENT_MARGINS = 15; + public const float CONTENT_MARGINS = 20; public const float TRANSITION_LENGTH = 600; @@ -106,17 +106,23 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, ExpandableHeader = CreateHeader(), SelectedSection = { BindTarget = CurrentSection }, - FixedHeader = searchTextBox = new SeekLimitedSearchTextBox + FixedHeader = new Container { RelativeSizeAxes = Axes.X, - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - Width = 0.95f, - Margin = new MarginPadding + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { - Top = 20, - Bottom = 20 + Vertical = 20, + Horizontal = CONTENT_MARGINS }, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Child = searchTextBox = new SeekLimitedSearchTextBox + { + RelativeSizeAxes = Axes.X, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + } }, Footer = CreateFooter().With(f => f.Alpha = 0) }); From 4c293b637f8219cfff0f2dcd22021f089760488e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 9 Oct 2021 19:20:44 +0200 Subject: [PATCH 303/391] Restyle settings panel header --- osu.Game/Overlays/Settings/SettingsHeader.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsHeader.cs b/osu.Game/Overlays/Settings/SettingsHeader.cs index a7f1cef74c..69b7b69a29 100644 --- a/osu.Game/Overlays/Settings/SettingsHeader.cs +++ b/osu.Game/Overlays/Settings/SettingsHeader.cs @@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Settings } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OverlayColourProvider colourProvider) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Settings new OsuSpriteText { Text = heading, - Font = OsuFont.GetFont(size: 40), + Font = OsuFont.TorusAlternate.With(size: 40), Margin = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Settings }, new OsuSpriteText { - Colour = colours.Pink, + Colour = colourProvider.Content2, Text = subheading, Font = OsuFont.GetFont(size: 18), Margin = new MarginPadding From e23a54f1e66149de2fa39c45e6baefaf0599184f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 9 Oct 2021 19:25:24 +0200 Subject: [PATCH 304/391] Adjust setting section appearance & spacings --- osu.Game/Overlays/Settings/SettingsSection.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsSection.cs b/osu.Game/Overlays/Settings/SettingsSection.cs index 33c0e9cafa..0381afe5ff 100644 --- a/osu.Game/Overlays/Settings/SettingsSection.cs +++ b/osu.Game/Overlays/Settings/SettingsSection.cs @@ -30,8 +30,7 @@ namespace osu.Game.Overlays.Settings public IEnumerable FilterableChildren => Children.OfType(); public virtual IEnumerable FilterTerms => new[] { Header.ToString() }; - private const int header_size = 26; - private const int margin = 20; + private const int header_size = 24; private const int border_size = 4; public bool MatchingFilter @@ -77,8 +76,8 @@ namespace osu.Game.Overlays.Settings { Padding = new MarginPadding { - Top = margin + border_size, - Bottom = margin + 10, + Top = 28, + Bottom = 40, }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -86,13 +85,11 @@ namespace osu.Game.Overlays.Settings { header = new OsuSpriteText { - Font = OsuFont.GetFont(size: header_size), + Font = OsuFont.TorusAlternate.With(size: header_size), Text = Header, - Colour = colours.Yellow, Margin = new MarginPadding { - Left = SettingsPanel.CONTENT_MARGINS, - Right = SettingsPanel.CONTENT_MARGINS + Horizontal = SettingsPanel.CONTENT_MARGINS } }, FlowContent From 855a74b8a075dcb66bb7652fd5fa72cc63297717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 9 Oct 2021 19:54:20 +0200 Subject: [PATCH 305/391] Adjust vertical spacings in individual subsections --- .../Settings/Sections/Input/TabletSettings.cs | 8 +++++++- .../Overlays/Settings/Sections/MaintenanceSection.cs | 2 -- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 3 --- osu.Game/Overlays/Settings/SettingsSection.cs | 6 +++++- osu.Game/Overlays/Settings/SettingsSubsection.cs | 12 ++++++++---- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs index 8c60e81fb5..c94b418331 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs @@ -165,7 +165,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input LabelText = TabletSettingsStrings.Rotation, Current = rotation }, - new RotationPresetButtons(tabletHandler), + new RotationPresetButtons(tabletHandler) + { + Padding = new MarginPadding + { + Horizontal = SettingsPanel.CONTENT_MARGINS + } + }, new SettingsSlider { TransferValueOnCommit = true, diff --git a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs index fa0c06167b..9410a87848 100644 --- a/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs +++ b/osu.Game/Overlays/Settings/Sections/MaintenanceSection.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Localisation; using osu.Game.Overlays.Settings.Sections.Maintenance; -using osuTK; namespace osu.Game.Overlays.Settings.Sections { @@ -21,7 +20,6 @@ namespace osu.Game.Overlays.Settings.Sections public MaintenanceSection() { - FlowContent.Spacing = new Vector2(0, 5); Children = new Drawable[] { new GeneralSettings() diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index e0d8252930..d18099eb0a 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -16,7 +16,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Skinning; using osu.Game.Skinning.Editor; -using osuTK; namespace osu.Game.Overlays.Settings.Sections { @@ -63,8 +62,6 @@ namespace osu.Game.Overlays.Settings.Sections [BackgroundDependencyLoader(permitNulls: true)] private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor) { - FlowContent.Spacing = new Vector2(0, 5); - Children = new Drawable[] { skinDropdown = new SkinSettingsDropdown(), diff --git a/osu.Game/Overlays/Settings/SettingsSection.cs b/osu.Game/Overlays/Settings/SettingsSection.cs index 0381afe5ff..0ae353602e 100644 --- a/osu.Game/Overlays/Settings/SettingsSection.cs +++ b/osu.Game/Overlays/Settings/SettingsSection.cs @@ -12,6 +12,7 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osuTK; namespace osu.Game.Overlays.Settings { @@ -30,6 +31,8 @@ namespace osu.Game.Overlays.Settings public IEnumerable FilterableChildren => Children.OfType(); public virtual IEnumerable FilterTerms => new[] { Header.ToString() }; + public const int ITEM_SPACING = 14; + private const int header_size = 24; private const int border_size = 4; @@ -52,8 +55,9 @@ namespace osu.Game.Overlays.Settings { Margin = new MarginPadding { - Top = header_size + Top = 36 }, + Spacing = new Vector2(0, ITEM_SPACING), Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, diff --git a/osu.Game/Overlays/Settings/SettingsSubsection.cs b/osu.Game/Overlays/Settings/SettingsSubsection.cs index 4aa9360452..c2cf08ac98 100644 --- a/osu.Game/Overlays/Settings/SettingsSubsection.cs +++ b/osu.Game/Overlays/Settings/SettingsSubsection.cs @@ -46,13 +46,17 @@ namespace osu.Game.Overlays.Settings FlowContent = new FillFlowContainer { + Margin = new MarginPadding { Top = SettingsSection.ITEM_SPACING }, Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 8), + Spacing = new Vector2(0, SettingsSection.ITEM_SPACING), RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, }; } + private const int header_height = 43; + private const int header_font_size = 20; + [BackgroundDependencyLoader] private void load() { @@ -60,9 +64,9 @@ namespace osu.Game.Overlays.Settings { new OsuSpriteText { - Text = Header.ToString().ToUpper(), // TODO: Add localisation support after https://github.com/ppy/osu-framework/pull/4603 is merged. - Margin = new MarginPadding { Vertical = 30, Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }, - Font = OsuFont.GetFont(weight: FontWeight.Bold), + Text = Header, + Margin = new MarginPadding { Vertical = (header_height - header_font_size) * 0.5f, Horizontal = SettingsPanel.CONTENT_MARGINS }, + Font = OsuFont.GetFont(size: header_font_size), }, FlowContent }); From b8616bf910335bf549522d1b09d38aa036f27619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 9 Oct 2021 20:29:44 +0200 Subject: [PATCH 306/391] Adjust appearance of keybinding subpanel --- .../Settings/Sections/Input/KeyBindingRow.cs | 21 +++++++++---------- .../Sections/Input/KeyBindingsSubsection.cs | 3 +-- osu.Game/Overlays/SettingsSubPanel.cs | 7 +++++-- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index cf8adf2785..da789db79a 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -78,7 +78,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private RealmContextFactory realmFactory { get; set; } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OverlayColourProvider colourProvider) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -101,7 +101,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input EdgeEffect = new EdgeEffectParameters { Radius = 2, - Colour = colours.YellowDark.Opacity(0), + Colour = colourProvider.Highlight1.Opacity(0), Type = EdgeEffectType.Shadow, Hollow = true, }, @@ -110,13 +110,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.6f, + Colour = colourProvider.Background5, }, text = new OsuSpriteText { Text = action.GetLocalisableDescription(), - Margin = new MarginPadding(padding), + Margin = new MarginPadding(1.5f * padding), }, buttons = new FillFlowContainer { @@ -405,7 +404,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly Box box; public readonly OsuSpriteText Text; - private Color4 hoverColour; + [Resolved] + private OverlayColourProvider colourProvider { get; set; } private bool isBinding; @@ -448,7 +448,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input box = new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4.Black }, Text = new OsuSpriteText { @@ -463,9 +462,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { - hoverColour = colours.YellowDark; + updateHoverState(); } protected override bool OnHover(HoverEvent e) @@ -484,12 +483,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input { if (isBinding) { - box.FadeColour(Color4.White, transition_time, Easing.OutQuint); + box.FadeColour(colourProvider.Light2, transition_time, Easing.OutQuint); Text.FadeColour(Color4.Black, transition_time, Easing.OutQuint); } else { - box.FadeColour(IsHovered ? hoverColour : Color4.Black, transition_time, Easing.OutQuint); + box.FadeColour(IsHovered ? colourProvider.Light4 : colourProvider.Background6, transition_time, Easing.OutQuint); Text.FadeColour(IsHovered ? Color4.Black : Color4.White, transition_time, Easing.OutQuint); } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index 2cc2857e9b..39dddbe1e6 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -27,8 +27,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { this.variant = variant; - FlowContent.Spacing = new Vector2(0, 1); - FlowContent.Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }; + FlowContent.Spacing = new Vector2(0, 3); } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/SettingsSubPanel.cs b/osu.Game/Overlays/SettingsSubPanel.cs index 1fa233d9d4..e63dcd299a 100644 --- a/osu.Game/Overlays/SettingsSubPanel.cs +++ b/osu.Game/Overlays/SettingsSubPanel.cs @@ -10,7 +10,6 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; using osuTK; -using osuTK.Graphics; namespace osu.Game.Overlays { @@ -36,12 +35,16 @@ namespace osu.Game.Overlays private class BackButton : OsuButton { + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + [BackgroundDependencyLoader] private void load() { Size = new Vector2(Sidebar.DEFAULT_WIDTH); - BackgroundColour = Color4.Black; + BackgroundColour = colourProvider.Background5; + Hover.Colour = Colour4.Transparent; AddRange(new Drawable[] { From fe26d8e8dfce3be34da60ef380d2d3184a76e7f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 9 Oct 2021 20:43:18 +0200 Subject: [PATCH 307/391] Share sidebar colouring logic by splitting out `SidebarIconButton` --- osu.Game/Overlays/Settings/Sidebar.cs | 8 +- osu.Game/Overlays/Settings/SidebarButton.cs | 120 ++--------------- .../Overlays/Settings/SidebarIconButton.cs | 126 ++++++++++++++++++ osu.Game/Overlays/SettingsPanel.cs | 6 +- osu.Game/Overlays/SettingsSubPanel.cs | 13 +- 5 files changed, 149 insertions(+), 124 deletions(-) create mode 100644 osu.Game/Overlays/Settings/SidebarIconButton.cs diff --git a/osu.Game/Overlays/Settings/Sidebar.cs b/osu.Game/Overlays/Settings/Sidebar.cs index 49585b3afb..93b1b19b17 100644 --- a/osu.Game/Overlays/Settings/Sidebar.cs +++ b/osu.Game/Overlays/Settings/Sidebar.cs @@ -16,16 +16,16 @@ using osuTK; namespace osu.Game.Overlays.Settings { - public class Sidebar : Container, IStateful + public class Sidebar : Container, IStateful { private readonly Box background; - private readonly FillFlowContainer content; + private readonly FillFlowContainer content; public const float DEFAULT_WIDTH = 70; public const int EXPANDED_WIDTH = 200; public event Action StateChanged; - protected override Container Content => content; + protected override Container Content => content; public Sidebar() { @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Settings { Children = new[] { - content = new FillFlowContainer + content = new FillFlowContainer { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs index a69b163998..197187e68c 100644 --- a/osu.Game/Overlays/Settings/SidebarButton.cs +++ b/osu.Game/Overlays/Settings/SidebarButton.cs @@ -1,144 +1,46 @@ -// 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 osuTK; 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.Input.Events; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.Settings { - public class SidebarButton : OsuButton + public abstract class SidebarButton : OsuButton { private const double fade_duration = 50; - private readonly ConstrainedIconContainer iconContainer; - private readonly SpriteText headerText; - private readonly CircularContainer selectionIndicator; - private readonly Container text; - [Resolved] - private OverlayColourProvider colourProvider { get; set; } + protected OverlayColourProvider ColourProvider { get; private set; } - // always consider as part of flow, even when not visible (for the sake of the initial animation). - public override bool IsPresent => true; - - private SettingsSection section; - - public SettingsSection Section - { - get => section; - set - { - section = value; - headerText.Text = value.Header; - iconContainer.Icon = value.CreateIcon(); - } - } - - private bool selected; - - public bool Selected - { - get => selected; - set - { - selected = value; - - if (IsLoaded) - updateState(); - } - } - - public SidebarButton() - { - RelativeSizeAxes = Axes.X; - Height = 46; - - AddRange(new Drawable[] - { - text = new Container - { - Width = Sidebar.DEFAULT_WIDTH, - RelativeSizeAxes = Axes.Y, - Colour = OsuColour.Gray(0.6f), - Children = new Drawable[] - { - headerText = new OsuSpriteText - { - Position = new Vector2(Sidebar.DEFAULT_WIDTH + 10, 0), - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - iconContainer = new ConstrainedIconContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(20), - }, - } - }, - selectionIndicator = new CircularContainer - { - Alpha = 0, - Width = 3, - Height = 18, - Masking = true, - CornerRadius = 1.5f, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding - { - Left = 9, - }, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Colour4.White - } - }, - }); - } + protected abstract Drawable HoverTarget { get; } [BackgroundDependencyLoader] private void load() { - BackgroundColour = colourProvider.Background5; - selectionIndicator.Colour = colourProvider.Highlight1; + BackgroundColour = ColourProvider.Background5; } protected override void LoadComplete() { base.LoadComplete(); - updateState(); + UpdateState(); + FinishTransforms(true); } protected override bool OnHover(HoverEvent e) { - updateState(); + UpdateState(); return false; } - protected override void OnHoverLost(HoverLostEvent e) => updateState(); + protected override void OnHoverLost(HoverLostEvent e) => UpdateState(); - private void updateState() + protected virtual void UpdateState() { - if (Selected) - { - text.FadeColour(colourProvider.Content1, fade_duration, Easing.OutQuint); - selectionIndicator.FadeIn(fade_duration, Easing.OutQuint); - return; - } - - text.FadeColour(IsHovered ? colourProvider.Light1 : colourProvider.Light3, fade_duration, Easing.OutQuint); - selectionIndicator.FadeOut(fade_duration, Easing.OutQuint); + HoverTarget.FadeColour(IsHovered ? ColourProvider.Light1 : ColourProvider.Light3, fade_duration, Easing.OutQuint); } } } diff --git a/osu.Game/Overlays/Settings/SidebarIconButton.cs b/osu.Game/Overlays/Settings/SidebarIconButton.cs new file mode 100644 index 0000000000..db580f4766 --- /dev/null +++ b/osu.Game/Overlays/Settings/SidebarIconButton.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 osuTK; +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.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.Containers; + +namespace osu.Game.Overlays.Settings +{ + public class SidebarIconButton : SidebarButton + { + private const double fade_duration = 50; + + private readonly ConstrainedIconContainer iconContainer; + private readonly SpriteText headerText; + private readonly CircularContainer selectionIndicator; + private readonly Container text; + + protected override Drawable HoverTarget => text; + + // always consider as part of flow, even when not visible (for the sake of the initial animation). + public override bool IsPresent => true; + + private SettingsSection section; + + public SettingsSection Section + { + get => section; + set + { + section = value; + headerText.Text = value.Header; + iconContainer.Icon = value.CreateIcon(); + } + } + + private bool selected; + + public bool Selected + { + get => selected; + set + { + selected = value; + + if (IsLoaded) + UpdateState(); + } + } + + public SidebarIconButton() + { + RelativeSizeAxes = Axes.X; + Height = 46; + + AddRange(new Drawable[] + { + text = new Container + { + Width = Sidebar.DEFAULT_WIDTH, + RelativeSizeAxes = Axes.Y, + Colour = OsuColour.Gray(0.6f), + Children = new Drawable[] + { + headerText = new OsuSpriteText + { + Position = new Vector2(Sidebar.DEFAULT_WIDTH + 10, 0), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + iconContainer = new ConstrainedIconContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(20), + }, + } + }, + selectionIndicator = new CircularContainer + { + Alpha = 0, + Width = 3, + Height = 18, + Masking = true, + CornerRadius = 1.5f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding + { + Left = 9, + }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.White + } + }, + }); + } + + [BackgroundDependencyLoader] + private void load() + { + selectionIndicator.Colour = ColourProvider.Highlight1; + } + + protected override void UpdateState() + { + if (Selected) + { + text.FadeColour(ColourProvider.Content1, fade_duration, Easing.OutQuint); + selectionIndicator.FadeIn(fade_duration, Easing.OutQuint); + return; + } + + selectionIndicator.FadeOut(fade_duration, Easing.OutQuint); + base.UpdateState(); + } + } +} diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 7c43875bef..0ceb7fc50d 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -44,7 +44,7 @@ namespace osu.Game.Overlays protected override Container Content => ContentContainer; protected Sidebar Sidebar; - private SidebarButton selectedSidebarButton; + private SidebarIconButton selectedSidebarButton; public SettingsSectionsContainer SectionsContainer { get; private set; } @@ -252,11 +252,11 @@ namespace osu.Game.Overlays }); } - private IEnumerable createSidebarButtons() + private IEnumerable createSidebarButtons() { foreach (var section in SectionsContainer) { - yield return new SidebarButton + yield return new SidebarIconButton { Section = section, Action = () => diff --git a/osu.Game/Overlays/SettingsSubPanel.cs b/osu.Game/Overlays/SettingsSubPanel.cs index e63dcd299a..25a245472b 100644 --- a/osu.Game/Overlays/SettingsSubPanel.cs +++ b/osu.Game/Overlays/SettingsSubPanel.cs @@ -7,7 +7,6 @@ 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.Overlays.Settings; using osuTK; @@ -33,22 +32,20 @@ namespace osu.Game.Overlays protected override bool DimMainContent => false; // dimming is handled by main overlay - private class BackButton : OsuButton + private class BackButton : SidebarButton { - [Resolved] - private OverlayColourProvider colourProvider { get; set; } + private Container content; + + protected override Drawable HoverTarget => content; [BackgroundDependencyLoader] private void load() { Size = new Vector2(Sidebar.DEFAULT_WIDTH); - BackgroundColour = colourProvider.Background5; - Hover.Colour = Colour4.Transparent; - AddRange(new Drawable[] { - new Container + content = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, From ccc6d8ff40bf04302b73c390b4b0c7b899644fdf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 10 Oct 2021 11:34:01 +0900 Subject: [PATCH 308/391] Improve the animation of the active indicator --- osu.Game/Overlays/Settings/SidebarIconButton.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Settings/SidebarIconButton.cs b/osu.Game/Overlays/Settings/SidebarIconButton.cs index db580f4766..d09873c3ea 100644 --- a/osu.Game/Overlays/Settings/SidebarIconButton.cs +++ b/osu.Game/Overlays/Settings/SidebarIconButton.cs @@ -15,7 +15,10 @@ namespace osu.Game.Overlays.Settings { public class SidebarIconButton : SidebarButton { - private const double fade_duration = 50; + private const double fade_duration = 500; + + private const float selection_indicator_height_active = 18; + private const float selection_indicator_height_inactive = 4; private readonly ConstrainedIconContainer iconContainer; private readonly SpriteText headerText; @@ -85,8 +88,8 @@ namespace osu.Game.Overlays.Settings selectionIndicator = new CircularContainer { Alpha = 0, - Width = 3, - Height = 18, + Width = 4, + Height = selection_indicator_height_inactive, Masking = true, CornerRadius = 1.5f, Anchor = Anchor.CentreLeft, @@ -116,10 +119,12 @@ namespace osu.Game.Overlays.Settings { text.FadeColour(ColourProvider.Content1, fade_duration, Easing.OutQuint); selectionIndicator.FadeIn(fade_duration, Easing.OutQuint); + selectionIndicator.ResizeHeightTo(selection_indicator_height_active, fade_duration, Easing.OutElasticHalf); return; } selectionIndicator.FadeOut(fade_duration, Easing.OutQuint); + selectionIndicator.ResizeHeightTo(selection_indicator_height_inactive, fade_duration, Easing.OutQuint); base.UpdateState(); } } From 49b341daffb677dd5de1c5d6d44101ac029d854b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 10 Oct 2021 11:55:45 +0900 Subject: [PATCH 309/391] Remove `HoverTarget` shared state update path Felt quite convoluted to follow. Have just duplicated the single shared line instead. --- osu.Game/Overlays/Settings/SidebarButton.cs | 10 ++----- .../Overlays/Settings/SidebarIconButton.cs | 27 +++++++++---------- osu.Game/Overlays/SettingsSubPanel.cs | 7 +++-- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/osu.Game/Overlays/Settings/SidebarButton.cs b/osu.Game/Overlays/Settings/SidebarButton.cs index 197187e68c..1a34143e1f 100644 --- a/osu.Game/Overlays/Settings/SidebarButton.cs +++ b/osu.Game/Overlays/Settings/SidebarButton.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.Framework.Input.Events; using osu.Game.Graphics.UserInterface; @@ -10,13 +9,11 @@ namespace osu.Game.Overlays.Settings { public abstract class SidebarButton : OsuButton { - private const double fade_duration = 50; + protected const double FADE_DURATION = 500; [Resolved] protected OverlayColourProvider ColourProvider { get; private set; } - protected abstract Drawable HoverTarget { get; } - [BackgroundDependencyLoader] private void load() { @@ -38,9 +35,6 @@ namespace osu.Game.Overlays.Settings protected override void OnHoverLost(HoverLostEvent e) => UpdateState(); - protected virtual void UpdateState() - { - HoverTarget.FadeColour(IsHovered ? ColourProvider.Light1 : ColourProvider.Light3, fade_duration, Easing.OutQuint); - } + protected abstract void UpdateState(); } } diff --git a/osu.Game/Overlays/Settings/SidebarIconButton.cs b/osu.Game/Overlays/Settings/SidebarIconButton.cs index d09873c3ea..fd57996b1b 100644 --- a/osu.Game/Overlays/Settings/SidebarIconButton.cs +++ b/osu.Game/Overlays/Settings/SidebarIconButton.cs @@ -15,17 +15,13 @@ namespace osu.Game.Overlays.Settings { public class SidebarIconButton : SidebarButton { - private const double fade_duration = 500; - private const float selection_indicator_height_active = 18; private const float selection_indicator_height_inactive = 4; private readonly ConstrainedIconContainer iconContainer; private readonly SpriteText headerText; private readonly CircularContainer selectionIndicator; - private readonly Container text; - - protected override Drawable HoverTarget => text; + private readonly Container textIconContent; // always consider as part of flow, even when not visible (for the sake of the initial animation). public override bool IsPresent => true; @@ -64,7 +60,7 @@ namespace osu.Game.Overlays.Settings AddRange(new Drawable[] { - text = new Container + textIconContent = new Container { Width = Sidebar.DEFAULT_WIDTH, RelativeSizeAxes = Axes.Y, @@ -117,15 +113,18 @@ namespace osu.Game.Overlays.Settings { if (Selected) { - text.FadeColour(ColourProvider.Content1, fade_duration, Easing.OutQuint); - selectionIndicator.FadeIn(fade_duration, Easing.OutQuint); - selectionIndicator.ResizeHeightTo(selection_indicator_height_active, fade_duration, Easing.OutElasticHalf); - return; - } + textIconContent.FadeColour(ColourProvider.Content1, FADE_DURATION, Easing.OutQuint); - selectionIndicator.FadeOut(fade_duration, Easing.OutQuint); - selectionIndicator.ResizeHeightTo(selection_indicator_height_inactive, fade_duration, Easing.OutQuint); - base.UpdateState(); + selectionIndicator.FadeIn(FADE_DURATION, Easing.OutQuint); + selectionIndicator.ResizeHeightTo(selection_indicator_height_active, FADE_DURATION, Easing.OutElasticHalf); + } + else + { + textIconContent.FadeColour(IsHovered ? ColourProvider.Light1 : ColourProvider.Light3, FADE_DURATION, Easing.OutQuint); + + selectionIndicator.FadeOut(FADE_DURATION, Easing.OutQuint); + selectionIndicator.ResizeHeightTo(selection_indicator_height_inactive, FADE_DURATION, Easing.OutQuint); + } } } } diff --git a/osu.Game/Overlays/SettingsSubPanel.cs b/osu.Game/Overlays/SettingsSubPanel.cs index 25a245472b..a65d792a9f 100644 --- a/osu.Game/Overlays/SettingsSubPanel.cs +++ b/osu.Game/Overlays/SettingsSubPanel.cs @@ -36,8 +36,6 @@ namespace osu.Game.Overlays { private Container content; - protected override Drawable HoverTarget => content; - [BackgroundDependencyLoader] private void load() { @@ -71,6 +69,11 @@ namespace osu.Game.Overlays } }); } + + protected override void UpdateState() + { + content.FadeColour(IsHovered ? ColourProvider.Light1 : ColourProvider.Light3, FADE_DURATION, Easing.OutQuint); + } } } } From c49d0a501335832ecef06f02a79750cdf6c1ec97 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 10 Oct 2021 15:43:24 +0900 Subject: [PATCH 310/391] Rewrite query to be easier to understand --- osu.Game/Scoring/ScoreManager.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 5481eea4aa..236e8a4448 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -74,10 +74,9 @@ namespace osu.Game.Scoring var totalScores = await Task.WhenAll(scores.Select(s => GetTotalScoreAsync(s, cancellationToken: cancellationToken))).ConfigureAwait(false); - return scores.Select((s, i) => (index: i, score: s)) - .OrderByDescending(key => totalScores[key.index]) - .ThenBy(key => key.score.OnlineScoreID) - .Select(key => key.score) + return scores.Select((score, index) => (score: score, totalScore: totalScores[index])) + .OrderByDescending(g => g.totalScore) + .Select(g => g.score) .ToArray(); } From 4475697a9c55897219fc79cf032f2f3da2b5d2ee Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 10 Oct 2021 15:47:39 +0900 Subject: [PATCH 311/391] Add score id key --- osu.Game/Scoring/ScoreManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 236e8a4448..c5d475d631 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -76,6 +76,7 @@ namespace osu.Game.Scoring return scores.Select((score, index) => (score: score, totalScore: totalScores[index])) .OrderByDescending(g => g.totalScore) + .ThenBy(g => g.score.OnlineScoreID) .Select(g => g.score) .ToArray(); } From 49a878dc20ce004169333f86021e02f528a6cf9b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 10 Oct 2021 16:04:41 +0900 Subject: [PATCH 312/391] Fix comma separator support not actually working --- osu.Game/Graphics/UserInterface/ScoreCounter.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs index 7ebf3819e4..069810d736 100644 --- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs +++ b/osu.Game/Graphics/UserInterface/ScoreCounter.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.Bindables; using osu.Framework.Graphics; using osu.Framework.Localisation; @@ -29,6 +30,9 @@ namespace osu.Game.Graphics.UserInterface { UseCommaSeparator = useCommaSeparator; + if (useCommaSeparator && leading > 0) + throw new ArgumentException("Should not mix leading zeroes and comma separators as it doesn't make sense"); + RequiredDisplayDigits.Value = leading; RequiredDisplayDigits.BindValueChanged(_ => UpdateDisplay()); } @@ -41,14 +45,15 @@ namespace osu.Game.Graphics.UserInterface protected override LocalisableString FormatCount(double count) { string format = new string('0', RequiredDisplayDigits.Value); + var output = ((long)count).ToString(format); if (UseCommaSeparator) { - for (int i = format.Length - 3; i > 0; i -= 3) - format = format.Insert(i, @","); + for (int i = output.Length - 3; i > 0; i -= 3) + output = output.Insert(i, @","); } - return ((long)count).ToString(format); + return output; } protected override OsuSpriteText CreateSpriteText() From 446f091d329db500d7915aa53d75bb8232a6a3f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 10 Oct 2021 16:06:12 +0900 Subject: [PATCH 313/391] Use comma separator for tournament score displays --- .../Gameplay/Components/TournamentMatchScoreDisplay.cs | 1 + osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs index 994dee4da0..3624c08187 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs @@ -132,6 +132,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private OsuSpriteText displayedSpriteText; public MatchScoreCounter() + : base(useCommaSeparator: true) { Margin = new MarginPadding { Top = bar_height, Horizontal = 10 }; } diff --git a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs index d04e60a2ab..37282205f4 100644 --- a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs +++ b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs @@ -4,11 +4,9 @@ using System; 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.Shapes; -using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -153,6 +151,7 @@ namespace osu.Game.Screens.Play.HUD private OsuSpriteText displayedSpriteText; public MatchScoreCounter() + : base(useCommaSeparator: true) { Margin = new MarginPadding { Top = bar_height, Horizontal = 10 }; } @@ -173,8 +172,6 @@ namespace osu.Game.Screens.Play.HUD => displayedSpriteText.Font = winning ? OsuFont.Torus.With(weight: FontWeight.Bold, size: font_size, fixedWidth: true) : OsuFont.Torus.With(weight: FontWeight.Regular, size: font_size * 0.8f, fixedWidth: true); - - protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(@"N0"); } } } From 392e7c4e73e255ac7ef32375c8061e1aee9af4ae Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 10 Oct 2021 16:16:59 +0900 Subject: [PATCH 314/391] Update tests --- .../OsuDifficultyCalculatorTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index b0d46f40fc..15675e74d1 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; - [TestCase(6.6634445062299665d, "diffcalc-test")] - [TestCase(1.0404303969295756d, "zero-length-sliders")] + [TestCase(6.5867229481955389d, "diffcalc-test")] + [TestCase(1.0416315570967911d, "zero-length-sliders")] public void Test(double expected, string name) => base.Test(expected, name); - [TestCase(8.3857915525197733d, "diffcalc-test")] - [TestCase(1.2705229071231638d, "zero-length-sliders")] + [TestCase(8.2730989071947896d, "diffcalc-test")] + [TestCase(1.2726413186221039d, "zero-length-sliders")] public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new OsuModDoubleTime()); From e30e5bd2147be7b846b83a02bf1452cb5a71a582 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 10 Oct 2021 16:23:35 +0900 Subject: [PATCH 315/391] Remove int casts in other calculators --- .../Difficulty/ManiaDifficultyCalculator.cs | 3 +-- .../Difficulty/TaikoDifficultyCalculator.cs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 9140e8afce..aee3268544 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -47,8 +47,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty { StarRating = skills[0].DifficultyValue() * star_scaling_factor, Mods = mods, - // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future - GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate), + GreatHitWindow = Math.Ceiling(getHitWindow300(mods) / clockRate), ScoreMultiplier = getScoreMultiplier(mods), MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1), Skills = skills diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index e755bb2325..7dd47e804b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -94,8 +94,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty StaminaStrain = staminaRating, RhythmStrain = rhythmRating, ColourStrain = colourRating, - // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future - GreatHitWindow = (int)hitWindows.WindowFor(HitResult.Great) / clockRate, + GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), Skills = skills }; From 6d6de5b677353235e9a667c17964ef30fb2d1b73 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 10 Oct 2021 16:50:55 +0900 Subject: [PATCH 316/391] Remove redundant tuple naming --- 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 c5d475d631..cf22a8fda4 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -74,7 +74,7 @@ namespace osu.Game.Scoring var totalScores = await Task.WhenAll(scores.Select(s => GetTotalScoreAsync(s, cancellationToken: cancellationToken))).ConfigureAwait(false); - return scores.Select((score, index) => (score: score, totalScore: totalScores[index])) + return scores.Select((score, index) => (score, totalScore: totalScores[index])) .OrderByDescending(g => g.totalScore) .ThenBy(g => g.score.OnlineScoreID) .Select(g => g.score) From 06cce0119c4549cd0f67c98fe08a617cda470040 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 10 Oct 2021 17:41:16 +0900 Subject: [PATCH 317/391] Use localisable format string for comma separator mode --- .../Graphics/UserInterface/ScoreCounter.cs | 39 ++++++++----------- .../Screens/Play/HUD/DefaultScoreCounter.cs | 1 - .../Screens/Play/HUD/GameplayScoreCounter.cs | 4 +- osu.Game/Skinning/LegacyScoreCounter.cs | 1 - 4 files changed, 19 insertions(+), 26 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs index 069810d736..778b0ecbc6 100644 --- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs +++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Bindables; +using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; @@ -14,13 +15,10 @@ namespace osu.Game.Graphics.UserInterface protected override double RollingDuration => 1000; protected override Easing RollingEasing => Easing.Out; - /// - /// Whether comma separators should be displayed. - /// - public bool UseCommaSeparator { get; } - public Bindable RequiredDisplayDigits { get; } = new Bindable(); + private string formatString = string.Empty; + /// /// Displays score. /// @@ -28,13 +26,22 @@ namespace osu.Game.Graphics.UserInterface /// Whether comma separators should be displayed. protected ScoreCounter(int leading = 0, bool useCommaSeparator = false) { - UseCommaSeparator = useCommaSeparator; + if (useCommaSeparator) + { + if (leading > 0) + throw new ArgumentException("Should not mix leading zeroes and comma separators as it doesn't make sense"); - if (useCommaSeparator && leading > 0) - throw new ArgumentException("Should not mix leading zeroes and comma separators as it doesn't make sense"); + formatString = @"N0"; + } RequiredDisplayDigits.Value = leading; - RequiredDisplayDigits.BindValueChanged(_ => UpdateDisplay()); + RequiredDisplayDigits.BindValueChanged(displayDigitsChanged, true); + } + + private void displayDigitsChanged(ValueChangedEvent _) + { + formatString = new string('0', RequiredDisplayDigits.Value); + UpdateDisplay(); } protected override double GetProportionalDuration(double currentValue, double newValue) @@ -42,19 +49,7 @@ namespace osu.Game.Graphics.UserInterface return currentValue > newValue ? currentValue - newValue : newValue - currentValue; } - protected override LocalisableString FormatCount(double count) - { - string format = new string('0', RequiredDisplayDigits.Value); - var output = ((long)count).ToString(format); - - if (UseCommaSeparator) - { - for (int i = output.Length - 3; i > 0; i -= 3) - output = output.Insert(i, @","); - } - - return output; - } + protected override LocalisableString FormatCount(double count) => ((long)count).ToLocalisableString(formatString); protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true)); diff --git a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs index 63de5c8de5..87b19e8433 100644 --- a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs @@ -11,7 +11,6 @@ namespace osu.Game.Screens.Play.HUD public class DefaultScoreCounter : GameplayScoreCounter, ISkinnableDrawable { public DefaultScoreCounter() - : base(6) { Anchor = Anchor.TopCentre; Origin = Anchor.TopCentre; diff --git a/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs b/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs index e09630d2c4..e05eff5f3e 100644 --- a/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/GameplayScoreCounter.cs @@ -14,8 +14,8 @@ namespace osu.Game.Screens.Play.HUD { private Bindable scoreDisplayMode; - protected GameplayScoreCounter(int leading = 0, bool useCommaSeparator = false) - : base(leading, useCommaSeparator) + protected GameplayScoreCounter() + : base(6) { } diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index a12defe87e..0c9a82074f 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -16,7 +16,6 @@ namespace osu.Game.Skinning public bool UsesFixedAnchor { get; set; } public LegacyScoreCounter() - : base(6) { Anchor = Anchor.TopRight; Origin = Anchor.TopRight; From 794b4c46cf9c93ddf9c1294a722f3e4114d11e0d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 10 Oct 2021 17:56:32 +0900 Subject: [PATCH 318/391] Split score counter class into two distinct classes to simplify usages --- .../Components/TournamentMatchScoreDisplay.cs | 3 +-- .../CommaSeparatedScoreCounter.cs | 24 +++++++++++++++++++ .../Graphics/UserInterface/ScoreCounter.cs | 20 ++++------------ .../Screens/Play/HUD/MatchScoreDisplay.cs | 3 +-- 4 files changed, 30 insertions(+), 20 deletions(-) create mode 100644 osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs index 3624c08187..77101e4023 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TournamentMatchScoreDisplay.cs @@ -127,12 +127,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components score2Text.X = Math.Max(5 + score2Text.DrawWidth / 2, score2Bar.DrawWidth); } - private class MatchScoreCounter : ScoreCounter + private class MatchScoreCounter : CommaSeparatedScoreCounter { private OsuSpriteText displayedSpriteText; public MatchScoreCounter() - : base(useCommaSeparator: true) { Margin = new MarginPadding { Top = bar_height, Horizontal = 10 }; } diff --git a/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs b/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.cs new file mode 100644 index 0000000000..4e1c612f09 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/CommaSeparatedScoreCounter.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.Extensions.LocalisationExtensions; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Graphics.UserInterface +{ + public abstract class CommaSeparatedScoreCounter : RollingCounter + { + protected override double RollingDuration => 1000; + protected override Easing RollingEasing => Easing.Out; + + protected override double GetProportionalDuration(double currentValue, double newValue) => + currentValue > newValue ? currentValue - newValue : newValue - currentValue; + + protected override LocalisableString FormatCount(double count) => ((long)count).ToLocalisableString(@"N0"); + + protected override OsuSpriteText CreateSpriteText() + => base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true)); + } +} diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs index 778b0ecbc6..25f19aa0a9 100644 --- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs +++ b/osu.Game/Graphics/UserInterface/ScoreCounter.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.Bindables; using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Graphics; @@ -17,23 +16,14 @@ namespace osu.Game.Graphics.UserInterface public Bindable RequiredDisplayDigits { get; } = new Bindable(); - private string formatString = string.Empty; + private string formatString; /// /// Displays score. /// /// How many leading zeroes the counter will have. - /// Whether comma separators should be displayed. - protected ScoreCounter(int leading = 0, bool useCommaSeparator = false) + protected ScoreCounter(int leading = 0) { - if (useCommaSeparator) - { - if (leading > 0) - throw new ArgumentException("Should not mix leading zeroes and comma separators as it doesn't make sense"); - - formatString = @"N0"; - } - RequiredDisplayDigits.Value = leading; RequiredDisplayDigits.BindValueChanged(displayDigitsChanged, true); } @@ -44,10 +34,8 @@ namespace osu.Game.Graphics.UserInterface UpdateDisplay(); } - protected override double GetProportionalDuration(double currentValue, double newValue) - { - return currentValue > newValue ? currentValue - newValue : newValue - currentValue; - } + protected override double GetProportionalDuration(double currentValue, double newValue) => + currentValue > newValue ? currentValue - newValue : newValue - currentValue; protected override LocalisableString FormatCount(double count) => ((long)count).ToLocalisableString(formatString); diff --git a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs index 37282205f4..b1c07512dd 100644 --- a/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs +++ b/osu.Game/Screens/Play/HUD/MatchScoreDisplay.cs @@ -146,12 +146,11 @@ namespace osu.Game.Screens.Play.HUD Score2Text.X = Math.Max(5 + Score2Text.DrawWidth / 2, score2Bar.DrawWidth); } - protected class MatchScoreCounter : ScoreCounter + protected class MatchScoreCounter : CommaSeparatedScoreCounter { private OsuSpriteText displayedSpriteText; public MatchScoreCounter() - : base(useCommaSeparator: true) { Margin = new MarginPadding { Top = bar_height, Horizontal = 10 }; } From 21ee24ea6d82ab5b8f42700f1829673f18d79add Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 10 Oct 2021 20:21:41 +0200 Subject: [PATCH 319/391] Add rounded button variant --- .../UserInterface/TestSceneRoundedButton.cs | 44 +++++++++++++++++ osu.Game/Graphics/OsuColour.cs | 5 ++ .../Graphics/UserInterfaceV2/RoundedButton.cs | 48 +++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs create mode 100644 osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.cs new file mode 100644 index 0000000000..9ccfba7c74 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRoundedButton.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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneRoundedButton : OsuTestScene + { + [Test] + public void TestBasic() + { + RoundedButton button = null; + + AddStep("create button", () => Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.DarkGray + }, + button = new RoundedButton + { + Width = 400, + Text = "Test button", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = () => { } + } + } + }); + + AddToggleStep("toggle disabled", disabled => button.Action = disabled ? (Action)null : () => { }); + } + } +} diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index d7cfc4094c..c1acdc392c 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -225,6 +225,11 @@ namespace osu.Game.Graphics public readonly Color4 GrayE = Color4Extensions.FromHex(@"eee"); public readonly Color4 GrayF = Color4Extensions.FromHex(@"fff"); + /// + /// Equivalent to 's . + /// + public readonly Color4 Blue3 = Color4Extensions.FromHex(@"3399cc"); + /// /// Equivalent to 's . /// diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs new file mode 100644 index 0000000000..3782320ead --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.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.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Effects; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public class RoundedButton : OsuButton + { + public override float Height + { + get => base.Height; + set + { + base.Height = value; + + if (IsLoaded) + updateCornerRadius(); + } + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.Blue3; + + Content.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(0, 2), + Radius = 4, + Colour = Colour4.Black.Opacity(0.15f) + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateCornerRadius(); + } + + private void updateCornerRadius() => Content.CornerRadius = DrawHeight / 2; + } +} From b30dd2d4ed97bdbf6cbdd3c06df9d95850405d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 10 Oct 2021 20:32:56 +0200 Subject: [PATCH 320/391] Use rounded button in settings sidebar --- osu.Game/Graphics/OsuColour.cs | 5 +++++ .../Graphics/UserInterfaceV2/RoundedButton.cs | 13 ++++++++++++- .../Settings/DangerousSettingsButton.cs | 5 +---- .../Sections/Input/KeyBindingsSubsection.cs | 3 +-- .../Sections/Maintenance/GeneralSettings.cs | 19 +++++++++---------- osu.Game/Overlays/Settings/SettingsButton.cs | 4 ++-- 6 files changed, 30 insertions(+), 19 deletions(-) diff --git a/osu.Game/Graphics/OsuColour.cs b/osu.Game/Graphics/OsuColour.cs index c1acdc392c..af2bb26871 100644 --- a/osu.Game/Graphics/OsuColour.cs +++ b/osu.Game/Graphics/OsuColour.cs @@ -225,6 +225,11 @@ namespace osu.Game.Graphics public readonly Color4 GrayE = Color4Extensions.FromHex(@"eee"); public readonly Color4 GrayF = Color4Extensions.FromHex(@"fff"); + /// + /// Equivalent to 's . + /// + public readonly Color4 Pink3 = Color4Extensions.FromHex(@"cc3378"); + /// /// Equivalent to 's . /// diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs index 3782320ead..5cbbc40405 100644 --- a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.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 System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Game.Graphics.UserInterface; using osuTK; namespace osu.Game.Graphics.UserInterfaceV2 { - public class RoundedButton : OsuButton + public class RoundedButton : OsuButton, IFilterable { public override float Height { @@ -44,5 +46,14 @@ namespace osu.Game.Graphics.UserInterfaceV2 } private void updateCornerRadius() => Content.CornerRadius = DrawHeight / 2; + + public virtual IEnumerable FilterTerms => new[] { Text.ToString() }; + + public bool MatchingFilter + { + set => this.FadeTo(value ? 1 : 0); + } + + public bool FilteringActive { get; set; } } } diff --git a/osu.Game/Overlays/Settings/DangerousSettingsButton.cs b/osu.Game/Overlays/Settings/DangerousSettingsButton.cs index c02db40eca..4ca3ace8a1 100644 --- a/osu.Game/Overlays/Settings/DangerousSettingsButton.cs +++ b/osu.Game/Overlays/Settings/DangerousSettingsButton.cs @@ -14,10 +14,7 @@ namespace osu.Game.Overlays.Settings [BackgroundDependencyLoader] private void load(OsuColour colours) { - BackgroundColour = colours.Pink; - - Triangles.ColourDark = colours.PinkDark; - Triangles.ColourLight = colours.PinkLight; + BackgroundColour = colours.Pink3; } } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index 39dddbe1e6..2051af6f3c 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Game.Database; -using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets; using osu.Game.Localisation; @@ -59,7 +58,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input } } - public class ResetButton : DangerousTriangleButton + public class ResetButton : DangerousSettingsButton { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 803c8332c1..43df58a8b1 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -10,7 +10,6 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Database; -using osu.Game.Graphics.UserInterface; using osu.Game.Localisation; using osu.Game.Scoring; using osu.Game.Skinning; @@ -21,15 +20,15 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { protected override LocalisableString Header => "General"; - private TriangleButton importBeatmapsButton; - private TriangleButton importScoresButton; - private TriangleButton importSkinsButton; - private TriangleButton importCollectionsButton; - private TriangleButton deleteBeatmapsButton; - private TriangleButton deleteScoresButton; - private TriangleButton deleteSkinsButton; - private TriangleButton restoreButton; - private TriangleButton undeleteButton; + private SettingsButton importBeatmapsButton; + private SettingsButton importScoresButton; + private SettingsButton importSkinsButton; + private SettingsButton importCollectionsButton; + private SettingsButton deleteBeatmapsButton; + private SettingsButton deleteScoresButton; + private SettingsButton deleteSkinsButton; + private SettingsButton restoreButton; + private SettingsButton undeleteButton; [BackgroundDependencyLoader(permitNulls: true)] private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, [CanBeNull] StableImportManager stableImportManager, DialogOverlay dialogOverlay) diff --git a/osu.Game/Overlays/Settings/SettingsButton.cs b/osu.Game/Overlays/Settings/SettingsButton.cs index 87b1aa0e46..be7f2de480 100644 --- a/osu.Game/Overlays/Settings/SettingsButton.cs +++ b/osu.Game/Overlays/Settings/SettingsButton.cs @@ -6,11 +6,11 @@ using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Localisation; -using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Overlays.Settings { - public class SettingsButton : TriangleButton, IHasTooltip + public class SettingsButton : RoundedButton, IHasTooltip { public SettingsButton() { From 3d6602b8df754fb8e1fefcb66d048b6854c336d8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Oct 2021 14:05:31 +0900 Subject: [PATCH 321/391] Ensure `FailAnimation` is disposed synchronously to avoid test failures --- osu.Game/Screens/Play/Player.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 090210e611..444bea049b 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -947,7 +947,7 @@ namespace osu.Game.Screens.Play public override void OnSuspending(IScreen next) { - screenSuspension?.Expire(); + screenSuspension?.RemoveAndDisposeImmediately(); fadeOut(); base.OnSuspending(next); @@ -955,7 +955,8 @@ namespace osu.Game.Screens.Play public override bool OnExiting(IScreen next) { - screenSuspension?.Expire(); + screenSuspension?.RemoveAndDisposeImmediately(); + failAnimation?.RemoveAndDisposeImmediately(); // if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap. if (prepareScoreForDisplayTask == null) From 6b35ccae95368c6e81aee1835d44cb945b320bc2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Oct 2021 17:01:37 +0900 Subject: [PATCH 322/391] Fix some cases where interface specifications can be used but weren't --- .../Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs | 6 +++--- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Beatmaps/BeatmapModelDownloader.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 79767bc671..558b874234 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -168,14 +168,14 @@ namespace osu.Game.Tests.Online return new TestBeatmapModelManager(this, storage, contextFactory, rulesets, api, host); } - protected override BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host) + protected override BeatmapModelDownloader CreateBeatmapModelDownloader(IBeatmapModelManager manager, IAPIProvider api, GameHost host) { - return new TestBeatmapModelDownloader(modelManager, api, host); + return new TestBeatmapModelDownloader(manager, api, host); } internal class TestBeatmapModelDownloader : BeatmapModelDownloader { - public TestBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost) + public TestBeatmapModelDownloader(IBeatmapModelManager modelManager, IAPIProvider apiProvider, GameHost gameHost) : base(modelManager, apiProvider, gameHost) { } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 240db22c00..14175f251b 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -54,7 +54,7 @@ namespace osu.Game.Beatmaps } } - protected virtual BeatmapModelDownloader CreateBeatmapModelDownloader(BeatmapModelManager modelManager, IAPIProvider api, GameHost host) + protected virtual BeatmapModelDownloader CreateBeatmapModelDownloader(IBeatmapModelManager modelManager, IAPIProvider api, GameHost host) { return new BeatmapModelDownloader(modelManager, api, host); } diff --git a/osu.Game/Beatmaps/BeatmapModelDownloader.cs b/osu.Game/Beatmaps/BeatmapModelDownloader.cs index ae482eeafd..30dc95a966 100644 --- a/osu.Game/Beatmaps/BeatmapModelDownloader.cs +++ b/osu.Game/Beatmaps/BeatmapModelDownloader.cs @@ -13,7 +13,7 @@ namespace osu.Game.Beatmaps protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => new DownloadBeatmapSetRequest(set, minimiseDownloadSize); - public BeatmapModelDownloader(BeatmapModelManager beatmapModelManager, IAPIProvider api, GameHost host = null) + public BeatmapModelDownloader(IBeatmapModelManager beatmapModelManager, IAPIProvider api, GameHost host = null) : base(beatmapModelManager, api, host) { } From c7675be3eff7130d7862ed26cbda6f1ecf05b2b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Oct 2021 14:20:21 +0900 Subject: [PATCH 323/391] Fix typo in `IModelImporter`'s xmldoc --- osu.Game/Database/IModelImporter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs index e94af01772..8e658cb0f5 100644 --- a/osu.Game/Database/IModelImporter.cs +++ b/osu.Game/Database/IModelImporter.cs @@ -10,7 +10,7 @@ using osu.Game.Overlays.Notifications; namespace osu.Game.Database { /// - /// A class which handles importing of asociated models to the game store. + /// A class which handles importing of associated models to the game store. /// /// The model type. public interface IModelImporter : IPostNotifications From b9460112929531743485ceaf1875fbd899b3f17d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Oct 2021 15:58:22 +0900 Subject: [PATCH 324/391] Update tests to run inside a `GameHost` to allow running on update thread --- osu.Game.Tests/Database/RealmTest.cs | 76 +++++++++++++++++++--------- 1 file changed, 51 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs index 576f901c1a..f5752aa606 100644 --- a/osu.Game.Tests/Database/RealmTest.cs +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -4,7 +4,6 @@ using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; -using Nito.AsyncEx; using NUnit.Framework; using osu.Framework.Logging; using osu.Framework.Platform; @@ -28,42 +27,69 @@ namespace osu.Game.Tests.Database protected void RunTestWithRealm(Action testAction, [CallerMemberName] string caller = "") { - AsyncContext.Run(() => + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller)) { - var testStorage = storage.GetStorageForDirectory(caller); - - using (var realmFactory = new RealmContextFactory(testStorage, caller)) + host.Run(new RealmTestGame(() => { - Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); - testAction(realmFactory, testStorage); + var testStorage = storage.GetStorageForDirectory(caller); - realmFactory.Dispose(); + using (var realmFactory = new RealmContextFactory(testStorage, caller)) + { + Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); + testAction(realmFactory, testStorage); - Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); - realmFactory.Compact(); - Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}"); - } - }); + realmFactory.Dispose(); + + Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); + realmFactory.Compact(); + Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}"); + } + })); + } } protected void RunTestWithRealmAsync(Func testAction, [CallerMemberName] string caller = "") { - AsyncContext.Run(async () => + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(caller)) { - var testStorage = storage.GetStorageForDirectory(caller); - - using (var realmFactory = new RealmContextFactory(testStorage, caller)) + host.Run(new RealmTestGame(async () => { - Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); - await testAction(realmFactory, testStorage); + var testStorage = storage.GetStorageForDirectory(caller); - realmFactory.Dispose(); + using (var realmFactory = new RealmContextFactory(testStorage, caller)) + { + Logger.Log($"Running test using realm file {testStorage.GetFullPath(realmFactory.Filename)}"); + await testAction(realmFactory, testStorage); - Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); - realmFactory.Compact(); - Logger.Log($"Final database size after compact: {getFileSize(testStorage, realmFactory)}"); - } - }); + realmFactory.Dispose(); + + Logger.Log($"Final database size: {getFileSize(testStorage, realmFactory)}"); + realmFactory.Compact(); + } + })); + } + } + + private class RealmTestGame : Framework.Game + { + public RealmTestGame(Func work) + { + // ReSharper disable once AsyncVoidLambda + Scheduler.Add(async () => + { + await work().ConfigureAwait(true); + Exit(); + }); + } + + public RealmTestGame(Action work) + { + Scheduler.Add(() => + { + work(); + Exit(); + }); + } } private static long getFileSize(Storage testStorage, RealmContextFactory realmFactory) From f43badabf408dc65fe90dca4f0cfa158c4cf4459 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Oct 2021 15:20:12 +0900 Subject: [PATCH 325/391] Add back update thread verification in `RealmContextFactory` --- osu.Game/Database/RealmContextFactory.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index 0ff902a8bc..c3810eb441 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -135,9 +135,8 @@ namespace osu.Game.Database if (IsDisposed) throw new ObjectDisposedException(nameof(RealmContextFactory)); - // TODO: this can be added for safety once we figure how to bypass in test - // if (!ThreadSafety.IsUpdateThread) - // throw new InvalidOperationException($"{nameof(BlockAllOperations)} must be called from the update thread."); + if (!ThreadSafety.IsUpdateThread) + throw new InvalidOperationException($"{nameof(BlockAllOperations)} must be called from the update thread."); Logger.Log(@"Blocking realm operations.", LoggingTarget.Database); From 6ca415da9f237635686504f1695b8792a719e5f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Oct 2021 15:25:00 +0900 Subject: [PATCH 326/391] Add basic realm models Only the file related ones are really required outside of tests, but seems like as good an opportunity as ever to get the rest of the models into the game project. --- osu.Game/Database/IHasRealmFiles.cs | 18 ++++ osu.Game/Database/INamedFile.cs | 17 +++ osu.Game/Models/RealmBeatmap.cs | 122 ++++++++++++++++++++++ osu.Game/Models/RealmBeatmapDifficulty.cs | 43 ++++++++ osu.Game/Models/RealmBeatmapMetadata.cs | 45 ++++++++ osu.Game/Models/RealmBeatmapSet.cs | 78 ++++++++++++++ osu.Game/Models/RealmFile.cs | 20 ++++ osu.Game/Models/RealmNamedFileUsage.cs | 32 ++++++ osu.Game/Models/RealmRuleset.cs | 63 +++++++++++ 9 files changed, 438 insertions(+) create mode 100644 osu.Game/Database/IHasRealmFiles.cs create mode 100644 osu.Game/Database/INamedFile.cs create mode 100644 osu.Game/Models/RealmBeatmap.cs create mode 100644 osu.Game/Models/RealmBeatmapDifficulty.cs create mode 100644 osu.Game/Models/RealmBeatmapMetadata.cs create mode 100644 osu.Game/Models/RealmBeatmapSet.cs create mode 100644 osu.Game/Models/RealmFile.cs create mode 100644 osu.Game/Models/RealmNamedFileUsage.cs create mode 100644 osu.Game/Models/RealmRuleset.cs diff --git a/osu.Game/Database/IHasRealmFiles.cs b/osu.Game/Database/IHasRealmFiles.cs new file mode 100644 index 0000000000..2adfe73d1e --- /dev/null +++ b/osu.Game/Database/IHasRealmFiles.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 System.Collections.Generic; +using osu.Game.Models; + +namespace osu.Game.Database +{ + /// + /// A model that contains a list of files it is responsible for. + /// + public interface IHasRealmFiles + { + IList Files { get; } + + string Hash { get; set; } + } +} diff --git a/osu.Game/Database/INamedFile.cs b/osu.Game/Database/INamedFile.cs new file mode 100644 index 0000000000..9c94aed38c --- /dev/null +++ b/osu.Game/Database/INamedFile.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.Models; + +namespace osu.Game.Database +{ + /// + /// Represent a join model which gives a filename and scope to a . + /// + public interface INamedFile + { + public string Filename { get; set; } + + public RealmFile File { get; set; } + } +} diff --git a/osu.Game/Models/RealmBeatmap.cs b/osu.Game/Models/RealmBeatmap.cs new file mode 100644 index 0000000000..09f8dafeb6 --- /dev/null +++ b/osu.Game/Models/RealmBeatmap.cs @@ -0,0 +1,122 @@ +// 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; +using Newtonsoft.Json; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Rulesets; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + /// + /// A single beatmap difficulty. + /// + [ExcludeFromDynamicCompile] + [Serializable] + [MapTo("Beatmap")] + public class RealmBeatmap : RealmObject, IHasGuidPrimaryKey, IBeatmapInfo + { + [PrimaryKey] + public Guid ID { get; set; } = Guid.NewGuid(); + + public string DifficultyName { get; set; } = string.Empty; + + public RealmRuleset Ruleset { get; set; } = null!; + + public RealmBeatmapDifficulty Difficulty { get; set; } = null!; + + public RealmBeatmapMetadata Metadata { get; set; } = null!; + + public RealmBeatmapSet? BeatmapSet { get; set; } + + public BeatmapSetOnlineStatus Status + { + get => (BeatmapSetOnlineStatus)StatusInt; + set => StatusInt = (int)value; + } + + [MapTo(nameof(Status))] + public int StatusInt { get; set; } + + public int? OnlineID { get; set; } + + public double Length { get; set; } + + public double BPM { get; set; } + + public string Hash { get; set; } = string.Empty; + + public double StarRating { get; set; } + + public string MD5Hash { get; set; } = string.Empty; + + [JsonIgnore] + public bool Hidden { get; set; } + + public RealmBeatmap(RealmRuleset ruleset, RealmBeatmapDifficulty difficulty, RealmBeatmapMetadata metadata) + { + Ruleset = ruleset; + Difficulty = difficulty; + Metadata = metadata; + } + + [UsedImplicitly] + private RealmBeatmap() + { + } + + #region Properties we may not want persisted (but also maybe no harm?) + + public double AudioLeadIn { get; set; } + + public float StackLeniency { get; set; } = 0.7f; + + public bool SpecialStyle { get; set; } + + public bool LetterboxInBreaks { get; set; } + + public bool WidescreenStoryboard { get; set; } + + public bool EpilepsyWarning { get; set; } + + public bool SamplesMatchPlaybackRate { get; set; } + + public double DistanceSpacing { get; set; } + + public int BeatDivisor { get; set; } + + public int GridSize { get; set; } + + public double TimelineZoom { get; set; } + + #endregion + + /// + /// Returns a shallow-clone of this . + /// + public RealmBeatmap Clone() => (RealmBeatmap)MemberwiseClone(); + + public bool AudioEquals(RealmBeatmap? other) => other != null + && BeatmapSet != null + && other.BeatmapSet != null + && BeatmapSet.Hash == other.BeatmapSet.Hash + && Metadata.AudioFile == other.Metadata.AudioFile; + + public bool BackgroundEquals(RealmBeatmap? other) => other != null + && BeatmapSet != null + && other.BeatmapSet != null + && BeatmapSet.Hash == other.BeatmapSet.Hash + && Metadata.BackgroundFile == other.Metadata.BackgroundFile; + + IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata; + IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet; + IRulesetInfo IBeatmapInfo.Ruleset => Ruleset; + IBeatmapDifficultyInfo IBeatmapInfo.Difficulty => Difficulty; + } +} diff --git a/osu.Game/Models/RealmBeatmapDifficulty.cs b/osu.Game/Models/RealmBeatmapDifficulty.cs new file mode 100644 index 0000000000..44bfdda491 --- /dev/null +++ b/osu.Game/Models/RealmBeatmapDifficulty.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.Game.Beatmaps; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [MapTo("BeatmapDifficulty")] + public class RealmBeatmapDifficulty : EmbeddedObject, IBeatmapDifficultyInfo + { + public float DrainRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; + public float CircleSize { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; + public float OverallDifficulty { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; + public float ApproachRate { get; set; } = IBeatmapDifficultyInfo.DEFAULT_DIFFICULTY; + + public double SliderMultiplier { get; set; } = 1; + public double SliderTickRate { get; set; } = 1; + + /// + /// Returns a shallow-clone of this . + /// + public RealmBeatmapDifficulty Clone() + { + var diff = new RealmBeatmapDifficulty(); + CopyTo(diff); + return diff; + } + + public void CopyTo(RealmBeatmapDifficulty difficulty) + { + difficulty.ApproachRate = ApproachRate; + difficulty.DrainRate = DrainRate; + difficulty.CircleSize = CircleSize; + difficulty.OverallDifficulty = OverallDifficulty; + + difficulty.SliderMultiplier = SliderMultiplier; + difficulty.SliderTickRate = SliderTickRate; + } + } +} diff --git a/osu.Game/Models/RealmBeatmapMetadata.cs b/osu.Game/Models/RealmBeatmapMetadata.cs new file mode 100644 index 0000000000..00dd120791 --- /dev/null +++ b/osu.Game/Models/RealmBeatmapMetadata.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; +using Newtonsoft.Json; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [ExcludeFromDynamicCompile] + [Serializable] + [MapTo("BeatmapMetadata")] + public class RealmBeatmapMetadata : RealmObject, IBeatmapMetadataInfo + { + public string Title { get; set; } = string.Empty; + + [JsonProperty("title_unicode")] + public string TitleUnicode { get; set; } = string.Empty; + + public string Artist { get; set; } = string.Empty; + + [JsonProperty("artist_unicode")] + public string ArtistUnicode { get; set; } = string.Empty; + + public string Author { get; set; } = string.Empty; // eventually should be linked to a persisted User. = string.Empty; + + public string Source { get; set; } = string.Empty; + + [JsonProperty(@"tags")] + public string Tags { get; set; } = string.Empty; + + /// + /// The time in milliseconds to begin playing the track for preview purposes. + /// If -1, the track should begin playing at 40% of its length. + /// + public int PreviewTime { get; set; } + + public string AudioFile { get; set; } = string.Empty; + public string BackgroundFile { get; set; } = string.Empty; + } +} diff --git a/osu.Game/Models/RealmBeatmapSet.cs b/osu.Game/Models/RealmBeatmapSet.cs new file mode 100644 index 0000000000..314ca4494b --- /dev/null +++ b/osu.Game/Models/RealmBeatmapSet.cs @@ -0,0 +1,78 @@ +// 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.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [ExcludeFromDynamicCompile] + [MapTo("BeatmapSet")] + public class RealmBeatmapSet : RealmObject, IHasGuidPrimaryKey, IHasRealmFiles, ISoftDelete, IEquatable, IBeatmapSetInfo + { + [PrimaryKey] + public Guid ID { get; set; } = Guid.NewGuid(); + + public int? OnlineID { get; set; } + + public DateTimeOffset DateAdded { get; set; } + + public IBeatmapMetadataInfo? Metadata => Beatmaps.FirstOrDefault()?.Metadata; + + public IList Beatmaps { get; } = null!; + + public IList Files { get; } = null!; + + public bool DeletePending { get; set; } + + public string Hash { get; set; } = string.Empty; + + /// + /// Whether deleting this beatmap set should be prohibited (due to it being a system requirement to be present). + /// + public bool Protected { get; set; } + + public double MaxStarDifficulty => Beatmaps.Max(b => b.StarRating); + + public double MaxLength => Beatmaps.Max(b => b.Length); + + public double MaxBPM => Beatmaps.Max(b => b.BPM); + + /// + /// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null. + /// The path returned is relative to the user file storage. + /// + /// The name of the file to get the storage path of. + public string? GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.StoragePath; + + public override string ToString() => Metadata?.ToString() ?? base.ToString(); + + public bool Equals(RealmBeatmapSet? other) + { + if (other == null) + return false; + + if (IsManaged && other.IsManaged) + return ID == other.ID; + + if (OnlineID.HasValue && other.OnlineID.HasValue) + return OnlineID == other.OnlineID; + + if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash)) + return Hash == other.Hash; + + return ReferenceEquals(this, other); + } + + IEnumerable IBeatmapSetInfo.Beatmaps => Beatmaps; + + IEnumerable IBeatmapSetInfo.Files => Files; + } +} diff --git a/osu.Game/Models/RealmFile.cs b/osu.Game/Models/RealmFile.cs new file mode 100644 index 0000000000..6836d79d2d --- /dev/null +++ b/osu.Game/Models/RealmFile.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 System.IO; +using osu.Game.IO; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [MapTo("File")] + public class RealmFile : RealmObject, IFileInfo + { + [PrimaryKey] + public string Hash { get; set; } = string.Empty; + + public string StoragePath => Path.Combine(Hash.Remove(1), Hash.Remove(2), Hash); + } +} diff --git a/osu.Game/Models/RealmNamedFileUsage.cs b/osu.Game/Models/RealmNamedFileUsage.cs new file mode 100644 index 0000000000..59b446112d --- /dev/null +++ b/osu.Game/Models/RealmNamedFileUsage.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 JetBrains.Annotations; +using osu.Game.Database; +using osu.Game.IO; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + public class RealmNamedFileUsage : EmbeddedObject, INamedFile, INamedFileUsage + { + public RealmFile File { get; set; } = null!; + + public string Filename { get; set; } = null!; + + public RealmNamedFileUsage(RealmFile file, string filename) + { + File = file; + Filename = filename; + } + + [UsedImplicitly] + private RealmNamedFileUsage() + { + } + + IFileInfo INamedFileUsage.File => File; + } +} diff --git a/osu.Game/Models/RealmRuleset.cs b/osu.Game/Models/RealmRuleset.cs new file mode 100644 index 0000000000..0dcd701ed2 --- /dev/null +++ b/osu.Game/Models/RealmRuleset.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 JetBrains.Annotations; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using Realms; + +#nullable enable + +namespace osu.Game.Models +{ + [ExcludeFromDynamicCompile] + [MapTo("Ruleset")] + public class RealmRuleset : RealmObject, IEquatable, IRulesetInfo + { + [PrimaryKey] + public string ShortName { get; set; } = string.Empty; + + public int? OnlineID { get; set; } + + public string Name { get; set; } = string.Empty; + + public string InstantiationInfo { get; set; } = string.Empty; + + public RealmRuleset(string shortName, string name, string instantiationInfo, int? onlineID = null) + { + ShortName = shortName; + Name = name; + InstantiationInfo = instantiationInfo; + OnlineID = onlineID; + } + + [UsedImplicitly] + private RealmRuleset() + { + } + + public RealmRuleset(int? onlineID, string name, string shortName, bool available) + { + OnlineID = onlineID; + Name = name; + ShortName = shortName; + Available = available; + } + + public bool Available { get; set; } + + public bool Equals(RealmRuleset? other) => other != null && OnlineID == other.OnlineID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; + + public override string ToString() => Name; + + public RealmRuleset Clone() => new RealmRuleset + { + OnlineID = OnlineID, + Name = Name, + ShortName = ShortName, + InstantiationInfo = InstantiationInfo, + Available = Available + }; + } +} From 03bf88ae813ad2b4026489d44fc8f9a6490936ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Oct 2021 15:26:16 +0900 Subject: [PATCH 327/391] Add realm `FileStore` and test coverage --- osu.Game.Tests/Database/FileStoreTests.cs | 114 ++++++++++++++++++++++ osu.Game.Tests/Database/RealmTest.cs | 42 ++++++++ osu.Game/Stores/RealmFileStore.cs | 113 +++++++++++++++++++++ 3 files changed, 269 insertions(+) create mode 100644 osu.Game.Tests/Database/FileStoreTests.cs create mode 100644 osu.Game/Stores/RealmFileStore.cs diff --git a/osu.Game.Tests/Database/FileStoreTests.cs b/osu.Game.Tests/Database/FileStoreTests.cs new file mode 100644 index 0000000000..861de5303d --- /dev/null +++ b/osu.Game.Tests/Database/FileStoreTests.cs @@ -0,0 +1,114 @@ +// 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.IO; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Logging; +using osu.Game.Models; +using osu.Game.Stores; + +#nullable enable + +namespace osu.Game.Tests.Database +{ + public class FileStoreTests : RealmTest + { + [Test] + public void TestImportFile() + { + RunTestWithRealm((realmFactory, storage) => + { + var realm = realmFactory.Context; + var files = new RealmFileStore(realmFactory, storage); + + var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 }); + + realm.Write(() => files.Add(testData, realm)); + + Assert.True(files.Storage.Exists("0/05/054edec1d0211f624fed0cbca9d4f9400b0e491c43742af2c5b0abebf0c990d8")); + Assert.True(files.Storage.Exists(realm.All().First().StoragePath)); + }); + } + + [Test] + public void TestImportSameFileTwice() + { + RunTestWithRealm((realmFactory, storage) => + { + var realm = realmFactory.Context; + var files = new RealmFileStore(realmFactory, storage); + + var testData = new MemoryStream(new byte[] { 0, 1, 2, 3 }); + + realm.Write(() => files.Add(testData, realm)); + realm.Write(() => files.Add(testData, realm)); + + Assert.AreEqual(1, realm.All().Count()); + }); + } + + [Test] + public void TestDontPurgeReferenced() + { + RunTestWithRealm((realmFactory, storage) => + { + var realm = realmFactory.Context; + var files = new RealmFileStore(realmFactory, storage); + + var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm)); + + var timer = new Stopwatch(); + timer.Start(); + + realm.Write(() => + { + // attach the file to an arbitrary beatmap + var beatmapSet = CreateBeatmapSet(CreateRuleset()); + + beatmapSet.Files.Add(new RealmNamedFileUsage(file, "arbitrary.resource")); + + realm.Add(beatmapSet); + }); + + Logger.Log($"Import complete at {timer.ElapsedMilliseconds}"); + + string path = file.StoragePath; + + Assert.True(realm.All().Any()); + Assert.True(files.Storage.Exists(path)); + + files.Cleanup(); + Logger.Log($"Cleanup complete at {timer.ElapsedMilliseconds}"); + + Assert.True(realm.All().Any()); + Assert.True(file.IsValid); + Assert.True(files.Storage.Exists(path)); + }); + } + + [Test] + public void TestPurgeUnreferenced() + { + RunTestWithRealm((realmFactory, storage) => + { + var realm = realmFactory.Context; + var files = new RealmFileStore(realmFactory, storage); + + var file = realm.Write(() => files.Add(new MemoryStream(new byte[] { 0, 1, 2, 3 }), realm)); + + string path = file.StoragePath; + + Assert.True(realm.All().Any()); + Assert.True(files.Storage.Exists(path)); + + files.Cleanup(); + + Assert.False(realm.All().Any()); + Assert.False(file.IsValid); + Assert.False(files.Storage.Exists(path)); + }); + } + } +} diff --git a/osu.Game.Tests/Database/RealmTest.cs b/osu.Game.Tests/Database/RealmTest.cs index f5752aa606..04c9f2577a 100644 --- a/osu.Game.Tests/Database/RealmTest.cs +++ b/osu.Game.Tests/Database/RealmTest.cs @@ -5,10 +5,12 @@ using System; using System.Runtime.CompilerServices; using System.Threading.Tasks; using NUnit.Framework; +using osu.Framework.Extensions; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Database; +using osu.Game.Models; #nullable enable @@ -70,6 +72,46 @@ namespace osu.Game.Tests.Database } } + protected static RealmBeatmapSet CreateBeatmapSet(RealmRuleset ruleset) + { + RealmFile createRealmFile() => new RealmFile { Hash = Guid.NewGuid().ToString().ComputeSHA2Hash() }; + + var metadata = new RealmBeatmapMetadata + { + Title = "My Love", + Artist = "Kuba Oms" + }; + + var beatmapSet = new RealmBeatmapSet + { + Beatmaps = + { + new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Easy", }, + new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Normal", }, + new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Hard", }, + new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata) { DifficultyName = "Insane", } + }, + Files = + { + new RealmNamedFileUsage(createRealmFile(), "test [easy].osu"), + new RealmNamedFileUsage(createRealmFile(), "test [normal].osu"), + new RealmNamedFileUsage(createRealmFile(), "test [hard].osu"), + new RealmNamedFileUsage(createRealmFile(), "test [insane].osu"), + } + }; + + for (int i = 0; i < 8; i++) + beatmapSet.Files.Add(new RealmNamedFileUsage(createRealmFile(), $"hitsound{i}.mp3")); + + foreach (var b in beatmapSet.Beatmaps) + b.BeatmapSet = beatmapSet; + + return beatmapSet; + } + + protected static RealmRuleset CreateRuleset() => + new RealmRuleset(0, "osu!", "osu", true); + private class RealmTestGame : Framework.Game { public RealmTestGame(Func work) diff --git a/osu.Game/Stores/RealmFileStore.cs b/osu.Game/Stores/RealmFileStore.cs new file mode 100644 index 0000000000..aac52b193c --- /dev/null +++ b/osu.Game/Stores/RealmFileStore.cs @@ -0,0 +1,113 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.IO; +using System.Linq; +using osu.Framework.Extensions; +using osu.Framework.IO.Stores; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Database; +using osu.Game.Models; +using Realms; + +#nullable enable + +namespace osu.Game.Stores +{ + /// + /// Handles the Store and retrieval of Files/FileSets to the database backing + /// + public class RealmFileStore + { + private readonly RealmContextFactory realmFactory; + public readonly IResourceStore Store; + + public Storage Storage; + + public RealmFileStore(RealmContextFactory realmFactory, Storage storage) + { + this.realmFactory = realmFactory; + + Storage = storage.GetStorageForDirectory(@"files"); + Store = new StorageBackedResourceStore(Storage); + } + + /// + /// Add a new file to the game-wide database, copying it to permanent storage if not already present. + /// + /// The file data stream. + /// The realm instance to add to. Should already be in a transaction. + /// + public RealmFile Add(Stream data, Realm realm) + { + string hash = data.ComputeSHA2Hash(); + + var existing = realm.Find(hash); + + var file = existing ?? new RealmFile { Hash = hash }; + + if (!checkFileExistsAndMatchesHash(file)) + copyToStore(file, data); + + if (!file.IsManaged) + realm.Add(file); + + return file; + } + + private void copyToStore(RealmFile file, Stream data) + { + data.Seek(0, SeekOrigin.Begin); + + using (var output = Storage.GetStream(file.StoragePath, FileAccess.Write)) + data.CopyTo(output); + + data.Seek(0, SeekOrigin.Begin); + } + + private bool checkFileExistsAndMatchesHash(RealmFile file) + { + string path = file.StoragePath; + + // we may be re-adding a file to fix missing store entries. + if (!Storage.Exists(path)) + return false; + + // even if the file already exists, check the existing checksum for safety. + using (var stream = Storage.GetStream(path)) + return stream.ComputeSHA2Hash() == file.Hash; + } + + public void Cleanup() + { + var realm = realmFactory.Context; + + // can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal. + using (var transaction = realm.BeginWrite()) + { + // TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707) + var files = realm.All().ToList(); + + foreach (var file in files) + { + if (file.BacklinksCount > 0) + continue; + + try + { + Storage.Delete(file.StoragePath); + realm.Remove(file); + } + catch (Exception e) + { + Logger.Error(e, $@"Could not delete databased file {file.Hash}"); + } + } + + transaction.Commit(); + } + } + } +} From b01d82b3fd8e05548a1eee87cc33c7b5920649df Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Sep 2021 23:46:16 +0900 Subject: [PATCH 328/391] Add `RealmLive` implementation --- osu.Game.Tests/Database/RealmLiveTests.cs | 199 +++++++++++++++++++++ osu.Game/Database/RealmLive.cs | 111 ++++++++++++ osu.Game/Database/RealmObjectExtensions.cs | 13 ++ 3 files changed, 323 insertions(+) create mode 100644 osu.Game.Tests/Database/RealmLiveTests.cs create mode 100644 osu.Game/Database/RealmLive.cs diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs new file mode 100644 index 0000000000..d6ea24e848 --- /dev/null +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -0,0 +1,199 @@ +// 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 System.Threading.Tasks; +using NUnit.Framework; +using osu.Game.Database; +using osu.Game.Models; +using Realms; + +#nullable enable + +namespace osu.Game.Tests.Database +{ + public class RealmLiveTests : RealmTest + { + [Test] + public void TestValueAccessWithOpenContext() + { + RunTestWithRealm((realmFactory, _) => + { + RealmLive? liveBeatmap = null; + Task.Factory.StartNew(() => + { + using (var threadContext = realmFactory.CreateContext()) + { + var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + + liveBeatmap = beatmap.ToLive(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + + Debug.Assert(liveBeatmap != null); + + Task.Factory.StartNew(() => + { + Assert.DoesNotThrow(() => + { + using (realmFactory.CreateContext()) + { + var resolved = liveBeatmap.Value; + + Assert.IsTrue(resolved.Realm.IsClosed); + Assert.IsTrue(resolved.IsValid); + + // can access properties without a crash. + Assert.IsFalse(resolved.Hidden); + } + }); + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + }); + } + + [Test] + public void TestScopedReadWithoutContext() + { + RunTestWithRealm((realmFactory, _) => + { + RealmLive? liveBeatmap = null; + Task.Factory.StartNew(() => + { + using (var threadContext = realmFactory.CreateContext()) + { + var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + + liveBeatmap = beatmap.ToLive(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + + Debug.Assert(liveBeatmap != null); + + Task.Factory.StartNew(() => + { + liveBeatmap.PerformRead(beatmap => + { + Assert.IsTrue(beatmap.IsValid); + Assert.IsFalse(beatmap.Hidden); + }); + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + }); + } + + [Test] + public void TestScopedWriteWithoutContext() + { + RunTestWithRealm((realmFactory, _) => + { + RealmLive? liveBeatmap = null; + Task.Factory.StartNew(() => + { + using (var threadContext = realmFactory.CreateContext()) + { + var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + + liveBeatmap = beatmap.ToLive(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + + Debug.Assert(liveBeatmap != null); + + Task.Factory.StartNew(() => + { + liveBeatmap.PerformWrite(beatmap => { beatmap.Hidden = true; }); + liveBeatmap.PerformRead(beatmap => { Assert.IsTrue(beatmap.Hidden); }); + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + }); + } + + [Test] + public void TestValueAccessWithoutOpenContextFails() + { + RunTestWithRealm((realmFactory, _) => + { + RealmLive? liveBeatmap = null; + Task.Factory.StartNew(() => + { + using (var threadContext = realmFactory.CreateContext()) + { + var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + + liveBeatmap = beatmap.ToLive(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + + Debug.Assert(liveBeatmap != null); + + Task.Factory.StartNew(() => + { + Assert.Throws(() => + { + var unused = liveBeatmap.Value; + }); + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + }); + } + + [Test] + public void TestLiveAssumptions() + { + RunTestWithRealm((realmFactory, _) => + { + int changesTriggered = 0; + + using (var updateThreadContext = realmFactory.CreateContext()) + { + updateThreadContext.All().SubscribeForNotifications(gotChange); + RealmLive? liveBeatmap = null; + + Task.Factory.StartNew(() => + { + using (var threadContext = realmFactory.CreateContext()) + { + var ruleset = CreateRuleset(); + var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + + // add a second beatmap to ensure that a full refresh occurs below. + // not just a refresh from the resolved Live. + threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))); + + liveBeatmap = beatmap.ToLive(); + } + }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait(); + + Debug.Assert(liveBeatmap != null); + + // not yet seen by main context + Assert.AreEqual(0, updateThreadContext.All().Count()); + Assert.AreEqual(0, changesTriggered); + + var resolved = liveBeatmap.Value; + + // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point. + Assert.AreEqual(2, updateThreadContext.All().Count()); + Assert.AreEqual(1, changesTriggered); + + // even though the realm that this instance was resolved for was closed, it's still valid. + Assert.IsTrue(resolved.Realm.IsClosed); + Assert.IsTrue(resolved.IsValid); + + // can access properties without a crash. + Assert.IsFalse(resolved.Hidden); + + updateThreadContext.Write(r => + { + // can use with the main context. + r.Remove(resolved); + }); + } + + void gotChange(IRealmCollection sender, ChangeSet changes, Exception error) + { + changesTriggered++; + } + }); + } + } +} diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs new file mode 100644 index 0000000000..71fb44f617 --- /dev/null +++ b/osu.Game/Database/RealmLive.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. + +using System; +using System.Threading; +using Realms; + +#nullable enable + +namespace osu.Game.Database +{ + /// + /// Provides a method of working with realm objects over longer application lifetimes. + /// + /// The underlying object type. + public class RealmLive : ILive where T : RealmObject, IHasGuidPrimaryKey + { + public Guid ID { get; } + + private readonly SynchronizationContext? fetchedContext; + private readonly int fetchedThreadId; + + /// + /// The original live data used to create this instance. + /// + private readonly T data; + + /// + /// Construct a new instance of live realm data. + /// + /// The realm data. + public RealmLive(T data) + { + this.data = data; + + fetchedContext = SynchronizationContext.Current; + fetchedThreadId = Thread.CurrentThread.ManagedThreadId; + + ID = data.ID; + } + + /// + /// Perform a read operation on this live object. + /// + /// The action to perform. + public void PerformRead(Action perform) + { + if (originalDataValid) + { + perform(data); + return; + } + + using (var realm = Realm.GetInstance(data.Realm.Config)) + perform(realm.Find(ID)); + } + + /// + /// Perform a read operation on this live object. + /// + /// The action to perform. + public TReturn PerformRead(Func perform) + { + if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn))) + throw new InvalidOperationException($"Realm live objects should not exit the scope of {nameof(PerformRead)}."); + + if (originalDataValid) + return perform(data); + + using (var realm = Realm.GetInstance(data.Realm.Config)) + return perform(realm.Find(ID)); + } + + /// + /// Perform a write operation on this live object. + /// + /// The action to perform. + public void PerformWrite(Action perform) => + PerformRead(t => + { + var transaction = t.Realm.BeginWrite(); + perform(t); + transaction.Commit(); + }); + + public T Value + { + get + { + if (originalDataValid) + return data; + + T retrieved; + + using (var realm = Realm.GetInstance(data.Realm.Config)) + retrieved = realm.Find(ID); + + if (!retrieved.IsValid) + throw new InvalidOperationException("Attempted to access value without an open context"); + + return retrieved; + } + } + + private bool originalDataValid => isCorrectThread && data.IsValid && !data.Realm.IsClosed; + + // this matches realm's internal thread validation (see https://github.com/realm/realm-dotnet/blob/903b4d0b304f887e37e2d905384fb572a6496e70/Realm/Realm/Native/SynchronizationContextScheduler.cs#L72) + private bool isCorrectThread + => (fetchedContext != null && SynchronizationContext.Current == fetchedContext) || fetchedThreadId == Thread.CurrentThread.ManagedThreadId; + } +} diff --git a/osu.Game/Database/RealmObjectExtensions.cs b/osu.Game/Database/RealmObjectExtensions.cs index c5aa1399a3..18a926fa8c 100644 --- a/osu.Game/Database/RealmObjectExtensions.cs +++ b/osu.Game/Database/RealmObjectExtensions.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 AutoMapper; using osu.Game.Input.Bindings; using Realms; @@ -47,5 +48,17 @@ namespace osu.Game.Database return mapper.Map(item); } + + public static List> ToLive(this IEnumerable realmList) + where T : RealmObject, IHasGuidPrimaryKey + { + return realmList.Select(l => new RealmLive(l)).ToList(); + } + + public static RealmLive ToLive(this T realmObject) + where T : RealmObject, IHasGuidPrimaryKey + { + return new RealmLive(realmObject); + } } } From 81a0fbfc40d2a6afca48fb64141eecabf5aab011 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 1 Oct 2021 14:30:27 +0900 Subject: [PATCH 329/391] Add `Live<>` casting test --- osu.Game.Tests/Database/RealmLiveTests.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs index d6ea24e848..33aa1afb89 100644 --- a/osu.Game.Tests/Database/RealmLiveTests.cs +++ b/osu.Game.Tests/Database/RealmLiveTests.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using NUnit.Framework; +using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Models; using Realms; @@ -16,6 +17,19 @@ namespace osu.Game.Tests.Database { public class RealmLiveTests : RealmTest { + [Test] + public void TestLiveCastability() + { + RunTestWithRealm((realmFactory, _) => + { + RealmLive beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive(); + + ILive iBeatmap = beatmap; + + Assert.AreEqual(0, iBeatmap.Value.Length); + }); + } + [Test] public void TestValueAccessWithOpenContext() { From 43aacb383170dcd854561c890fa8c7d6c6f5e1f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Oct 2021 16:11:15 +0900 Subject: [PATCH 330/391] Fix two different skins displaying at the same time when rapidly switching --- osu.Game/Skinning/SkinnableTargetContainer.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinnableTargetContainer.cs b/osu.Game/Skinning/SkinnableTargetContainer.cs index e7125bb034..20c2fcc075 100644 --- a/osu.Game/Skinning/SkinnableTargetContainer.cs +++ b/osu.Game/Skinning/SkinnableTargetContainer.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using System.Threading; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -22,6 +23,8 @@ namespace osu.Game.Skinning public bool ComponentsLoaded { get; private set; } + private CancellationTokenSource cancellationSource; + public SkinnableTargetContainer(SkinnableTarget target) { Target = target; @@ -38,6 +41,9 @@ namespace osu.Game.Skinning content = CurrentSkin.GetDrawableComponent(new SkinnableTargetComponent(Target)) as SkinnableTargetComponentsContainer; + cancellationSource?.Cancel(); + cancellationSource = null; + if (content != null) { LoadComponentAsync(content, wrapper => @@ -45,7 +51,7 @@ namespace osu.Game.Skinning AddInternal(wrapper); components.AddRange(wrapper.Children.OfType()); ComponentsLoaded = true; - }); + }, (cancellationSource = new CancellationTokenSource()).Token); } else ComponentsLoaded = true; From b4092549c03f9a2d3076899518e26bb460e013d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Oct 2021 16:35:36 +0900 Subject: [PATCH 331/391] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 5a0e7479fa..fefc2f6438 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4877ddf725..ff382f5227 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index edce9d27fe..fff0cbf418 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From 484a95229e64383a94a12851a5814047465a1b47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Oct 2021 16:36:04 +0900 Subject: [PATCH 332/391] Update toast implementations temporarily to expedite getting tests back in line --- osu.Game/Overlays/OSD/TrackedSettingToast.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/OSD/TrackedSettingToast.cs b/osu.Game/Overlays/OSD/TrackedSettingToast.cs index 51214fe460..198aa1438a 100644 --- a/osu.Game/Overlays/OSD/TrackedSettingToast.cs +++ b/osu.Game/Overlays/OSD/TrackedSettingToast.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.OSD private Sample sampleChange; public TrackedSettingToast(SettingDescription description) - : base(description.Name, description.Value, description.Shortcut) + : base(description.Name.ToString(), description.Value.ToString(), description.Shortcut.ToString()) { FillFlowContainer optionLights; From 22e90076fb5e2c91ef55fa49d3c059ac69253c63 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Oct 2021 18:05:45 +0900 Subject: [PATCH 333/391] Add temporary logging --- osu.Game/Audio/Effects/AudioFilter.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Audio/Effects/AudioFilter.cs b/osu.Game/Audio/Effects/AudioFilter.cs index ee48bdd7d9..7a51d7be8f 100644 --- a/osu.Game/Audio/Effects/AudioFilter.cs +++ b/osu.Game/Audio/Effects/AudioFilter.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.Diagnostics; using ManagedBass.Fx; using osu.Framework.Audio.Mixing; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Logging; namespace osu.Game.Audio.Effects { @@ -130,8 +132,15 @@ namespace osu.Game.Audio.Effects { base.Dispose(isDisposing); - if (mixer.Effects.Contains(filter)) - detachFilter(); + try + { + if (mixer.Effects.Contains(filter)) + detachFilter(); + } + catch (Exception e) + { + Logger.Log($"Exception in audio filter disposal: {e}"); + } } } } From b1ad3161dd33119d57cf42715aae1f74a1157be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 11 Oct 2021 21:25:02 +0200 Subject: [PATCH 334/391] Add failing test case for frame stable clock direction flip scenario --- .../TestSceneFrameStabilityContainer.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs index 5eb71e92c2..881e3f097c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs @@ -103,6 +103,30 @@ namespace osu.Game.Tests.Visual.Gameplay checkFrameCount(0); } + [Test] + public void TestSeekToSameTimePreservesRate() + { + AddStep("set manual clock rate", () => manualClock.Rate = 1); + seekManualTo(5000); + createStabilityContainer(); + checkRate(1); + + seekManualTo(10000); + checkRate(1); + + seekManualTo(10000); + checkRate(1); + + seekManualTo(5000); + checkRate(-1); + + seekManualTo(5000); + checkRate(-1); + + seekManualTo(10000); + checkRate(1); + } + private const int max_frames_catchup = 50; private void createStabilityContainer(double gameplayStartTime = double.MinValue) => AddStep("create container", () => @@ -116,6 +140,9 @@ namespace osu.Game.Tests.Visual.Gameplay private void checkFrameCount(int frames) => AddAssert($"elapsed frames is {frames}", () => consumer.ElapsedFrames == frames); + private void checkRate(double rate) => + AddAssert($"clock rate is {rate}", () => consumer.Clock.Rate == rate); + public class ClockConsumingChild : CompositeDrawable { private readonly OsuSpriteText text; From 56eae703fed67c5694004bf4d33d24b3ccb8dbad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 11 Oct 2021 21:39:48 +0200 Subject: [PATCH 335/391] Avoid changing frame stable clock direction if time hasn't changed between frames --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index e9865f6c8b..c0b339a231 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -55,7 +55,10 @@ namespace osu.Game.Rulesets.UI /// /// The current direction of playback to be exposed to frame stable children. /// - private int direction; + /// + /// Initially it is presumed that playback will proceed in the forward direction. + /// + private int direction = 1; [BackgroundDependencyLoader(true)] private void load(GameplayClock clock, ISamplePlaybackDisabler sampleDisabler) @@ -139,7 +142,9 @@ namespace osu.Game.Rulesets.UI state = PlaybackState.NotValid; } - if (state == PlaybackState.Valid) + // if the proposed time is the same as the current time, assume that the clock will continue progressing in the same direction as previously. + // this avoids spurious flips in direction from -1 to 1 during rewinds. + if (state == PlaybackState.Valid && proposedTime != manualClock.CurrentTime) direction = proposedTime >= manualClock.CurrentTime ? 1 : -1; double timeBehind = Math.Abs(proposedTime - parentGameplayClock.CurrentTime); From ff382259ca2048be6399eacc31b79df491e96dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 11 Oct 2021 23:10:56 +0200 Subject: [PATCH 336/391] Use rounded buttons in tablet rotation preset settings --- .../Settings/TestSceneTabletSettings.cs | 4 + .../Sections/Input/RotationPresetButtons.cs | 74 ++++++++++++------- 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs index 997eac709d..dc5b0e0d77 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs @@ -3,6 +3,7 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Handlers.Tablet; @@ -21,6 +22,9 @@ namespace osu.Game.Tests.Visual.Settings private TestTabletHandler tabletHandler; private TabletSettings settings; + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + [SetUpSteps] public void SetUpSteps() { diff --git a/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs b/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs index 26610628d5..3ef5ce8941 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/RotationPresetButtons.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 osu.Framework.Allocation; using osu.Framework.Bindables; @@ -8,16 +9,24 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Handlers.Tablet; using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Overlays.Settings.Sections.Input { - internal class RotationPresetButtons : FillFlowContainer + internal class RotationPresetButtons : CompositeDrawable { + public new MarginPadding Padding + { + get => base.Padding; + set => base.Padding = value; + } + private readonly ITabletHandler tabletHandler; private Bindable rotation; + private readonly RotationButton[] rotationPresets = new RotationButton[preset_count]; + private const int preset_count = 4; private const int height = 50; public RotationPresetButtons(ITabletHandler tabletHandler) @@ -27,18 +36,39 @@ namespace osu.Game.Overlays.Settings.Sections.Input RelativeSizeAxes = Axes.X; Height = height; - for (int i = 0; i < 360; i += 90) + IEnumerable createColumns(int count) { - var presetRotation = i; - - Add(new RotationButton(i) + for (int i = 0; i < count; ++i) { - RelativeSizeAxes = Axes.X, - Height = height, - Width = 0.25f, - Text = $@"{presetRotation}º", - Action = () => tabletHandler.Rotation.Value = presetRotation, - }); + if (i > 0) + yield return new Dimension(GridSizeMode.Absolute, 10); + + yield return new Dimension(); + } + } + + GridContainer grid; + + InternalChild = grid = new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = createColumns(preset_count).ToArray() + }; + + grid.Content = new[] { new Drawable[preset_count * 2 - 1] }; + + for (int i = 0; i < preset_count; i++) + { + var rotationValue = i * 90; + + var rotationPreset = new RotationButton(rotationValue) + { + RelativeSizeAxes = Axes.Both, + Height = 1, + Text = $@"{rotationValue}º", + Action = () => tabletHandler.Rotation.Value = rotationValue, + }; + grid.Content[0][2 * i] = rotationPresets[i] = rotationPreset; } } @@ -49,16 +79,19 @@ namespace osu.Game.Overlays.Settings.Sections.Input rotation = tabletHandler.Rotation.GetBoundCopy(); rotation.BindValueChanged(val => { - foreach (var b in Children.OfType()) + foreach (var b in rotationPresets) b.IsSelected = b.Preset == val.NewValue; }, true); } - public class RotationButton : TriangleButton + public class RotationButton : RoundedButton { [Resolved] private OsuColour colours { get; set; } + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + public readonly int Preset; public RotationButton(int preset) @@ -91,18 +124,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private void updateColour() { - if (isSelected) - { - BackgroundColour = colours.BlueDark; - Triangles.ColourDark = colours.BlueDarker; - Triangles.ColourLight = colours.Blue; - } - else - { - BackgroundColour = colours.Gray4; - Triangles.ColourDark = colours.Gray5; - Triangles.ColourLight = colours.Gray6; - } + BackgroundColour = isSelected ? colours.Blue3 : colourProvider.Background3; } } } From 1550a3b470d44c93e809c9cf68d45bee7eac5e87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 11:11:55 +0900 Subject: [PATCH 337/391] Rethrow exception after logging to make tracking on CI easier --- osu.Game/Audio/Effects/AudioFilter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Audio/Effects/AudioFilter.cs b/osu.Game/Audio/Effects/AudioFilter.cs index 7a51d7be8f..5eaa87af8d 100644 --- a/osu.Game/Audio/Effects/AudioFilter.cs +++ b/osu.Game/Audio/Effects/AudioFilter.cs @@ -140,6 +140,7 @@ namespace osu.Game.Audio.Effects catch (Exception e) { Logger.Log($"Exception in audio filter disposal: {e}"); + throw; } } } From df83f0db08d3d66765febfe17ed4af84cb504f70 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 11:28:11 +0900 Subject: [PATCH 338/391] Fix cross-thread list manipulation in `SkinProvidingContainer` --- osu.Game/Skinning/SkinProvidingContainer.cs | 27 ++++++++------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index ada6e4b788..d2c3a6e837 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; @@ -43,7 +44,7 @@ namespace osu.Game.Skinning /// /// A dictionary mapping each source to a wrapper which handles lookup allowances. /// - private readonly List<(ISkin skin, DisableableSkinSource wrapped)> skinSources = new List<(ISkin, DisableableSkinSource)>(); + private (ISkin skin, DisableableSkinSource wrapped)[] skinSources = Array.Empty<(ISkin skin, DisableableSkinSource wrapped)>(); /// /// Constructs a new initialised with a single skin source. @@ -173,32 +174,24 @@ namespace osu.Game.Skinning /// The skin to add. protected void AddSource(ISkin skin) { - skinSources.Add((skin, new DisableableSkinSource(skin, this))); + skinSources = skinSources.Append((skin, new DisableableSkinSource(skin, this))).ToArray(); if (skin is ISkinSource source) source.SourceChanged += TriggerSourceChanged; } - /// - /// Remove a skin from this provider. - /// - /// The skin to remove. - protected void RemoveSource(ISkin skin) - { - if (skinSources.RemoveAll(s => s.skin == skin) == 0) - return; - - if (skin is ISkinSource source) - source.SourceChanged -= TriggerSourceChanged; - } - /// /// Clears all skin sources. /// protected void ResetSources() { - foreach (var i in skinSources.ToArray()) - RemoveSource(i.skin); + foreach (var skin in skinSources) + { + if (skin.skin is ISkinSource source) + source.SourceChanged -= TriggerSourceChanged; + } + + skinSources = Array.Empty<(ISkin skin, DisableableSkinSource wrapped)>(); } /// From e0c54e3207811a14f90c94ac6e13fd0eea00cac4 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Tue, 12 Oct 2021 09:37:11 +0700 Subject: [PATCH 339/391] add `OpenChangelog` link action --- osu.Game/Online/Chat/MessageFormatter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 0e4ea694aa..cfffac7741 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -324,6 +324,7 @@ namespace osu.Game.Online.Chat SearchBeatmapSet, OpenWiki, Custom, + OpenChangelog, } public class Link : IComparable From 39a3482458b586787e0612503f55a77463a57d7f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 11:55:04 +0900 Subject: [PATCH 340/391] Replace Add/Reset methods with single `Set` method --- .../Skins/TestSceneSkinProvidingContainer.cs | 4 +-- .../Skinning/RulesetSkinProvidingContainer.cs | 5 +--- osu.Game/Skinning/SkinProvidingContainer.cs | 29 ++++++++----------- 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs index ab47067411..2247f685e2 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs @@ -6,7 +6,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; @@ -67,8 +66,7 @@ namespace osu.Game.Tests.Skins protected override void OnSourceChanged() { - ResetSources(); - sources.ForEach(AddSource); + SetSources(sources); } } diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index f5a7788359..e63af0dfbd 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -60,8 +60,6 @@ namespace osu.Game.Skinning protected override void OnSourceChanged() { - ResetSources(); - // Populate a local list first so we can adjust the returned order as we go. var sources = new List(); @@ -91,8 +89,7 @@ namespace osu.Game.Skinning else sources.Add(rulesetResourcesSkin); - foreach (var skin in sources) - AddSource(skin); + SetSources(sources); } protected ISkin GetLegacyRulesetTransformedSkin(ISkin legacySkin) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index d2c3a6e837..b769332e37 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -53,7 +53,7 @@ namespace osu.Game.Skinning : this() { if (skin != null) - AddSource(skin); + SetSources(new[] { skin }); } /// @@ -169,21 +169,10 @@ namespace osu.Game.Skinning } /// - /// Add a new skin to this provider. Will be added to the end of the lookup order precedence. + /// Replace the sources used for lookups in this container. /// - /// The skin to add. - protected void AddSource(ISkin skin) - { - skinSources = skinSources.Append((skin, new DisableableSkinSource(skin, this))).ToArray(); - - if (skin is ISkinSource source) - source.SourceChanged += TriggerSourceChanged; - } - - /// - /// Clears all skin sources. - /// - protected void ResetSources() + /// The new sources. + protected void SetSources(IEnumerable sources) { foreach (var skin in skinSources) { @@ -191,11 +180,17 @@ namespace osu.Game.Skinning source.SourceChanged -= TriggerSourceChanged; } - skinSources = Array.Empty<(ISkin skin, DisableableSkinSource wrapped)>(); + skinSources = sources.Select(skin => (skin, new DisableableSkinSource(skin, this))).ToArray(); + + foreach (var skin in skinSources) + { + if (skin.skin is ISkinSource source) + source.SourceChanged += TriggerSourceChanged; + } } /// - /// Invoked when any source has changed (either or a source registered via ). + /// Invoked when any source has changed (either or sources replaced via ). /// This is also invoked once initially during to ensure sources are ready for children consumption. /// protected virtual void OnSourceChanged() { } From 7fcb01bdf162beb4a53270cd90a7a6927dec2de1 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Tue, 12 Oct 2021 09:37:42 +0700 Subject: [PATCH 341/391] add changelog links test --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 2c2c4dc24e..af87fc17ad 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -509,5 +509,17 @@ namespace osu.Game.Tests.Chat Assert.AreEqual(LinkAction.External, result.Action); Assert.AreEqual("/relative", result.Argument); } + + [TestCase("https://dev.ppy.sh/home/changelog", "")] + [TestCase("https://dev.ppy.sh/home/changelog/lazer/2021.1012", "lazer/2021.1012")] + public void TestChangelogLinks(string link, string expectedArg) + { + MessageFormatter.WebsiteRootUrl = "dev.ppy.sh"; + + LinkDetails result = MessageFormatter.GetLinkDetails(link); + + Assert.AreEqual(LinkAction.OpenChangelog, result.Action); + Assert.AreEqual(expectedArg, result.Argument); + } } } From 47c7701e47be09d20f9c86475715477a9bc5b333 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Tue, 12 Oct 2021 09:40:45 +0700 Subject: [PATCH 342/391] handle changelog link in message formatter --- osu.Game/Online/Chat/MessageFormatter.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index cfffac7741..201ba6239b 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -177,6 +177,24 @@ namespace osu.Game.Online.Chat case "wiki": return new LinkDetails(LinkAction.OpenWiki, string.Join('/', args.Skip(3))); + + case "home": + if (mainArg != "changelog") + // handle link other than changelog as external for now + return new LinkDetails(LinkAction.External, url); + + switch (args.Length) + { + case 4: + // https://osu.ppy.sh/home/changelog + return new LinkDetails(LinkAction.OpenChangelog, string.Empty); + + case 6: + // https://osu.ppy.sh/home/changelog/lazer/2021.1006 + return new LinkDetails(LinkAction.OpenChangelog, $"{args[4]}/{args[5]}"); + } + + break; } } From 80722c7dc79d33ca948ccbf0aae91de4c919f472 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Tue, 12 Oct 2021 09:41:59 +0700 Subject: [PATCH 343/391] change `changelogOverlay` to field in `OsuGame` --- osu.Game/OsuGame.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 8a018f17d9..d233afe3aa 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -90,6 +90,8 @@ namespace osu.Game private WikiOverlay wikiOverlay; + private ChangelogOverlay changelogOverlay; + private SkinEditorOverlay skinEditor; private Container overlayContent; @@ -769,7 +771,7 @@ namespace osu.Game loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true); loadComponentSingleFile(new MessageNotifier(), AddInternal, true); loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, true); - var changelogOverlay = loadComponentSingleFile(new ChangelogOverlay(), overlayContent.Add, true); + loadComponentSingleFile(changelogOverlay = new ChangelogOverlay(), overlayContent.Add, true); loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true); loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true); loadComponentSingleFile(wikiOverlay = new WikiOverlay(), overlayContent.Add, true); From 6c84cf66589128096b9701b337eabc5d18520ac4 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Tue, 12 Oct 2021 09:42:29 +0700 Subject: [PATCH 344/391] add `ShowChangelogListing` and `ShowChangelogBuild` --- osu.Game/OsuGame.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index d233afe3aa..9ce049e1a1 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -403,6 +403,18 @@ namespace osu.Game /// The wiki page to show public void ShowWiki(string path) => waitForReady(() => wikiOverlay, _ => wikiOverlay.ShowPage(path)); + /// + /// Show changelog listing overlay + /// + public void ShowChangelogListing() => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowListing()); + + /// + /// Show changelog's build as an overlay + /// + /// The update stream name + /// The build version of the update stream + public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version)); + /// /// Present a beatmap at song select immediately. /// The user should have already requested this interactively. From 81246a110cc47d56875dc54134e2071eac22cb6b Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Rosfatiputra Date: Tue, 12 Oct 2021 09:43:32 +0700 Subject: [PATCH 345/391] add `OpenChangelog` in `OsuGame.HandleLink` --- osu.Game/OsuGame.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 9ce049e1a1..7895715045 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -338,6 +338,17 @@ namespace osu.Game ShowWiki(link.Argument); break; + case LinkAction.OpenChangelog: + if (string.IsNullOrEmpty(link.Argument)) + ShowChangelogListing(); + else + { + var changelogArgs = link.Argument.Split("/"); + ShowChangelogBuild(changelogArgs[0], changelogArgs[1]); + } + + break; + default: throw new NotImplementedException($"This {nameof(LinkAction)} ({link.Action.ToString()}) is missing an associated action."); } From 077dcf5cd99d98be377356250c2cbdf1f8f1bbe3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 12:50:28 +0900 Subject: [PATCH 346/391] Add missing documentation for `SourceChanged` --- osu.Game/Skinning/ISkinSource.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Skinning/ISkinSource.cs b/osu.Game/Skinning/ISkinSource.cs index ba3e2bf6ad..a5ed0fc990 100644 --- a/osu.Game/Skinning/ISkinSource.cs +++ b/osu.Game/Skinning/ISkinSource.cs @@ -12,6 +12,9 @@ namespace osu.Game.Skinning /// public interface ISkinSource : ISkin { + /// + /// Fired whenever a source change occurs, signalling that consumers should re-query as required. + /// event Action SourceChanged; /// From a849e7343edd8cbf01e58fab98fd63c3c9e6b560 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 13:04:18 +0900 Subject: [PATCH 347/391] Add lock to ensure no threading shenanigans --- osu.Game/Skinning/SkinProvidingContainer.cs | 23 +++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index b769332e37..fd80992000 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -41,6 +41,8 @@ namespace osu.Game.Skinning protected virtual bool AllowColourLookup => true; + private readonly object sourceSetLock = new object(); + /// /// A dictionary mapping each source to a wrapper which handles lookup allowances. /// @@ -174,18 +176,21 @@ namespace osu.Game.Skinning /// The new sources. protected void SetSources(IEnumerable sources) { - foreach (var skin in skinSources) + lock (sourceSetLock) { - if (skin.skin is ISkinSource source) - source.SourceChanged -= TriggerSourceChanged; - } + foreach (var skin in skinSources) + { + if (skin.skin is ISkinSource source) + source.SourceChanged -= TriggerSourceChanged; + } - skinSources = sources.Select(skin => (skin, new DisableableSkinSource(skin, this))).ToArray(); + skinSources = sources.Select(skin => (skin, new DisableableSkinSource(skin, this))).ToArray(); - foreach (var skin in skinSources) - { - if (skin.skin is ISkinSource source) - source.SourceChanged += TriggerSourceChanged; + foreach (var skin in skinSources) + { + if (skin.skin is ISkinSource source) + source.SourceChanged += TriggerSourceChanged; + } } } From d7cbacc5a066947d6500313dcd296df43364a2e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 13:04:48 +0900 Subject: [PATCH 348/391] Rename `OnSourceChanged` and expand on xmldoc to mention that it doesn't fire `SourceChanged` --- osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs | 2 +- osu.Game/Skinning/RulesetSkinProvidingContainer.cs | 2 +- osu.Game/Skinning/SkinProvidingContainer.cs | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs index 2247f685e2..ffb3d41d18 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinProvidingContainer.cs @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Skins public new void TriggerSourceChanged() => base.TriggerSourceChanged(); - protected override void OnSourceChanged() + protected override void RefreshSources() { SetSources(sources); } diff --git a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs index e63af0dfbd..b884794739 100644 --- a/osu.Game/Skinning/RulesetSkinProvidingContainer.cs +++ b/osu.Game/Skinning/RulesetSkinProvidingContainer.cs @@ -58,7 +58,7 @@ namespace osu.Game.Skinning return base.CreateChildDependencies(parent); } - protected override void OnSourceChanged() + protected override void RefreshSources() { // Populate a local list first so we can adjust the returned order as we go. var sources = new List(); diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index fd80992000..c8e4c2c7b6 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -173,6 +173,9 @@ namespace osu.Game.Skinning /// /// Replace the sources used for lookups in this container. /// + /// + /// This does not implicitly fire a event. Consider calling if required. + /// /// The new sources. protected void SetSources(IEnumerable sources) { @@ -195,15 +198,15 @@ namespace osu.Game.Skinning } /// - /// Invoked when any source has changed (either or sources replaced via ). + /// Invoked after any consumed source change, before the external event is fired. /// This is also invoked once initially during to ensure sources are ready for children consumption. /// - protected virtual void OnSourceChanged() { } + protected virtual void RefreshSources() { } protected void TriggerSourceChanged() { // Expose to implementations, giving them a chance to react before notifying external consumers. - OnSourceChanged(); + RefreshSources(); SourceChanged?.Invoke(); } From e982f485c7bd5df017892b7d8dafeaa1f4d49a5e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 13:17:48 +0900 Subject: [PATCH 349/391] Remove drop shadow from `RoundedButton` As per @arflyte's spec, this should not have been there in the first place. --- osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs index 5cbbc40405..27e28f1e03 100644 --- a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs @@ -5,9 +5,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osu.Game.Graphics.UserInterface; -using osuTK; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -29,14 +27,6 @@ namespace osu.Game.Graphics.UserInterfaceV2 private void load(OsuColour colours) { BackgroundColour = colours.Blue3; - - Content.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Offset = new Vector2(0, 2), - Radius = 4, - Colour = Colour4.Black.Opacity(0.15f) - }; } protected override void LoadComplete() From a986870a998762f635e27b149e9b04d66d18dd6e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 13:41:35 +0900 Subject: [PATCH 350/391] Reorder sections to be more in line with how often they are adjusted --- osu.Game/Overlays/SettingsOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 55e8aee266..af91677adb 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -24,12 +24,12 @@ namespace osu.Game.Overlays protected override IEnumerable CreateSections() => new SettingsSection[] { new GeneralSection(), - new GraphicsSection(), - new AudioSection(), + new SkinSection(), new InputSection(createSubPanel(new KeyBindingPanel())), new UserInterfaceSection(), new GameplaySection(), - new SkinSection(), + new AudioSection(), + new GraphicsSection(), new OnlineSection(), new MaintenanceSection(), new DebugSection(), From 1d3d67c5f1ba160698380077061698122a8c2d4b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 13:56:10 +0900 Subject: [PATCH 351/391] Move gameplay cursor settings to gameplay section --- .../Settings/Sections/Gameplay/GeneralSettings.cs | 11 +++++++++++ osu.Game/Overlays/Settings/Sections/SkinSection.cs | 11 ----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 3a0265e453..44b2a28d28 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -81,6 +81,17 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay Current = config.GetBindable(OsuSetting.ScoreDisplayMode), Keywords = new[] { "scoring" } }, + new SettingsSlider + { + LabelText = SkinSettingsStrings.GameplayCursorSize, + Current = config.GetBindable(OsuSetting.GameplayCursorSize), + KeyboardStep = 0.01f + }, + new SettingsCheckbox + { + LabelText = SkinSettingsStrings.AutoCursorSize, + Current = config.GetBindable(OsuSetting.AutoCursorSize) + }, }; if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index d18099eb0a..929936acb4 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -71,17 +71,6 @@ namespace osu.Game.Overlays.Settings.Sections Action = () => skinEditor?.Toggle(), }, new ExportSkinButton(), - new SettingsSlider - { - LabelText = SkinSettingsStrings.GameplayCursorSize, - Current = config.GetBindable(OsuSetting.GameplayCursorSize), - KeyboardStep = 0.01f - }, - new SettingsCheckbox - { - LabelText = SkinSettingsStrings.AutoCursorSize, - Current = config.GetBindable(OsuSetting.AutoCursorSize) - }, new SettingsCheckbox { LabelText = SkinSettingsStrings.BeatmapSkins, From 8285f065c20a09ec3db33862f5d7f5270a2ac2f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 14:26:57 +0900 Subject: [PATCH 352/391] Reorganise gameplay settings into more sections --- .../Localisation/GameplaySettingsStrings.cs | 25 ++++ .../Sections/Gameplay/AudioSettings.cs | 34 ++++++ .../Sections/Gameplay/BackgroundSettings.cs | 48 ++++++++ .../Sections/Gameplay/BeatmapSettings.cs | 39 +++++++ .../Sections/Gameplay/GeneralSettings.cs | 107 ------------------ .../Settings/Sections/Gameplay/HUDSettings.cs | 52 +++++++++ .../Sections/Gameplay/InputSettings.cs | 45 ++++++++ .../Settings/Sections/GameplaySection.cs | 6 +- .../Overlays/Settings/Sections/SkinSection.cs | 15 --- 9 files changed, 248 insertions(+), 123 deletions(-) create mode 100644 osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs create mode 100644 osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs create mode 100644 osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs delete mode 100644 osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs create mode 100644 osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs create mode 100644 osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs diff --git a/osu.Game/Localisation/GameplaySettingsStrings.cs b/osu.Game/Localisation/GameplaySettingsStrings.cs index 6d6381b429..fa92187650 100644 --- a/osu.Game/Localisation/GameplaySettingsStrings.cs +++ b/osu.Game/Localisation/GameplaySettingsStrings.cs @@ -14,11 +14,36 @@ namespace osu.Game.Localisation /// public static LocalisableString GameplaySectionHeader => new TranslatableString(getKey(@"gameplay_section_header"), @"Gameplay"); + /// + /// "Beatmap" + /// + public static LocalisableString BeatmapHeader => new TranslatableString(getKey(@"beatmap_header"), @"Beatmap"); + /// /// "General" /// public static LocalisableString GeneralHeader => new TranslatableString(getKey(@"general_header"), @"General"); + /// + /// "Audio" + /// + public static LocalisableString AudioHeader => new TranslatableString(getKey(@"audio"), @"Audio"); + + /// + /// "HUD" + /// + public static LocalisableString HUDHeader => new TranslatableString(getKey(@"h_u_d"), @"HUD"); + + /// + /// "Input" + /// + public static LocalisableString InputHeader => new TranslatableString(getKey(@"input"), @"Input"); + + /// + /// "Background" + /// + public static LocalisableString BackgroundHeader => new TranslatableString(getKey(@"background"), @"Background"); + /// /// "Background dim" /// diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.cs new file mode 100644 index 0000000000..dba64d695a --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/AudioSettings.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.Graphics; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Settings.Sections.Gameplay +{ + public class AudioSettings : SettingsSubsection + { + protected override LocalisableString Header => GameplaySettingsStrings.AudioHeader; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + Children = new Drawable[] + { + new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.PositionalHitsounds, + Current = config.GetBindable(OsuSetting.PositionalHitSounds) + }, + new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.AlwaysPlayFirstComboBreak, + Current = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak) + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.cs new file mode 100644 index 0000000000..94e0c5e494 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/BackgroundSettings.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.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Settings.Sections.Gameplay +{ + public class BackgroundSettings : SettingsSubsection + { + protected override LocalisableString Header => GameplaySettingsStrings.BackgroundHeader; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + Children = new Drawable[] + { + new SettingsSlider + { + LabelText = GameplaySettingsStrings.BackgroundDim, + Current = config.GetBindable(OsuSetting.DimLevel), + KeyboardStep = 0.01f, + DisplayAsPercentage = true + }, + new SettingsSlider + { + LabelText = GameplaySettingsStrings.BackgroundBlur, + Current = config.GetBindable(OsuSetting.BlurLevel), + KeyboardStep = 0.01f, + DisplayAsPercentage = true + }, + new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.LightenDuringBreaks, + Current = config.GetBindable(OsuSetting.LightenDuringBreaks) + }, + new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.FadePlayfieldWhenHealthLow, + Current = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow), + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs new file mode 100644 index 0000000000..a5d1fc5fc3 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs @@ -0,0 +1,39 @@ +// 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.Localisation; +using osu.Game.Configuration; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Settings.Sections.Gameplay +{ + public class BeatmapSettings : SettingsSubsection + { + protected override LocalisableString Header => GameplaySettingsStrings.BeatmapHeader; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + Children = new Drawable[] + { + new SettingsCheckbox + { + LabelText = SkinSettingsStrings.BeatmapSkins, + Current = config.GetBindable(OsuSetting.BeatmapSkins) + }, + new SettingsCheckbox + { + LabelText = SkinSettingsStrings.BeatmapColours, + Current = config.GetBindable(OsuSetting.BeatmapColours) + }, + new SettingsCheckbox + { + LabelText = SkinSettingsStrings.BeatmapHitsounds, + Current = config.GetBindable(OsuSetting.BeatmapHitsounds) + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs deleted file mode 100644 index 44b2a28d28..0000000000 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ /dev/null @@ -1,107 +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; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Localisation; -using osu.Game.Configuration; -using osu.Game.Localisation; -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Overlays.Settings.Sections.Gameplay -{ - public class GeneralSettings : SettingsSubsection - { - protected override LocalisableString Header => GameplaySettingsStrings.GeneralHeader; - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - Children = new Drawable[] - { - new SettingsSlider - { - LabelText = GameplaySettingsStrings.BackgroundDim, - Current = config.GetBindable(OsuSetting.DimLevel), - KeyboardStep = 0.01f, - DisplayAsPercentage = true - }, - new SettingsSlider - { - LabelText = GameplaySettingsStrings.BackgroundBlur, - Current = config.GetBindable(OsuSetting.BlurLevel), - KeyboardStep = 0.01f, - DisplayAsPercentage = true - }, - new SettingsCheckbox - { - LabelText = GameplaySettingsStrings.LightenDuringBreaks, - Current = config.GetBindable(OsuSetting.LightenDuringBreaks) - }, - new SettingsEnumDropdown - { - LabelText = GameplaySettingsStrings.HUDVisibilityMode, - Current = config.GetBindable(OsuSetting.HUDVisibilityMode) - }, - new SettingsCheckbox - { - LabelText = GameplaySettingsStrings.ShowDifficultyGraph, - Current = config.GetBindable(OsuSetting.ShowProgressGraph) - }, - new SettingsCheckbox - { - LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail, - Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), - Keywords = new[] { "hp", "bar" } - }, - new SettingsCheckbox - { - LabelText = GameplaySettingsStrings.FadePlayfieldWhenHealthLow, - Current = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow), - }, - new SettingsCheckbox - { - LabelText = GameplaySettingsStrings.AlwaysShowKeyOverlay, - Current = config.GetBindable(OsuSetting.KeyOverlay) - }, - new SettingsCheckbox - { - LabelText = GameplaySettingsStrings.PositionalHitsounds, - Current = config.GetBindable(OsuSetting.PositionalHitSounds) - }, - new SettingsCheckbox - { - LabelText = GameplaySettingsStrings.AlwaysPlayFirstComboBreak, - Current = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak) - }, - new SettingsEnumDropdown - { - LabelText = GameplaySettingsStrings.ScoreDisplayMode, - Current = config.GetBindable(OsuSetting.ScoreDisplayMode), - Keywords = new[] { "scoring" } - }, - new SettingsSlider - { - LabelText = SkinSettingsStrings.GameplayCursorSize, - Current = config.GetBindable(OsuSetting.GameplayCursorSize), - KeyboardStep = 0.01f - }, - new SettingsCheckbox - { - LabelText = SkinSettingsStrings.AutoCursorSize, - Current = config.GetBindable(OsuSetting.AutoCursorSize) - }, - }; - - if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) - { - Add(new SettingsCheckbox - { - LabelText = GameplaySettingsStrings.DisableWinKey, - Current = config.GetBindable(OsuSetting.GameplayDisableWinKey) - }); - } - } - } -} diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs new file mode 100644 index 0000000000..2ffd19a020 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Localisation; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Overlays.Settings.Sections.Gameplay +{ + public class HUDSettings : SettingsSubsection + { + protected override LocalisableString Header => GameplaySettingsStrings.HUDHeader; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + Children = new Drawable[] + { + new SettingsEnumDropdown + { + LabelText = GameplaySettingsStrings.HUDVisibilityMode, + Current = config.GetBindable(OsuSetting.HUDVisibilityMode) + }, + new SettingsEnumDropdown + { + LabelText = GameplaySettingsStrings.ScoreDisplayMode, + Current = config.GetBindable(OsuSetting.ScoreDisplayMode), + Keywords = new[] { "scoring" } + }, + new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.ShowDifficultyGraph, + Current = config.GetBindable(OsuSetting.ShowProgressGraph) + }, + new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.ShowHealthDisplayWhenCantFail, + Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), + Keywords = new[] { "hp", "bar" } + }, + new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.AlwaysShowKeyOverlay, + Current = config.GetBindable(OsuSetting.KeyOverlay) + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.cs new file mode 100644 index 0000000000..962572ca6e --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/InputSettings.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; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Settings.Sections.Gameplay +{ + public class InputSettings : SettingsSubsection + { + protected override LocalisableString Header => GameplaySettingsStrings.InputHeader; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + Children = new Drawable[] + { + new SettingsSlider + { + LabelText = SkinSettingsStrings.GameplayCursorSize, + Current = config.GetBindable(OsuSetting.GameplayCursorSize), + KeyboardStep = 0.01f + }, + new SettingsCheckbox + { + LabelText = SkinSettingsStrings.AutoCursorSize, + Current = config.GetBindable(OsuSetting.AutoCursorSize) + }, + }; + + if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) + { + Add(new SettingsCheckbox + { + LabelText = GameplaySettingsStrings.DisableWinKey, + Current = config.GetBindable(OsuSetting.GameplayDisableWinKey) + }); + } + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index 42d9d48d73..c4da641574 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs @@ -27,7 +27,11 @@ namespace osu.Game.Overlays.Settings.Sections { Children = new Drawable[] { - new GeneralSettings(), + new AudioSettings(), + new BeatmapSettings(), + new BackgroundSettings(), + new HUDSettings(), + new InputSettings(), new ModsSettings(), }; } diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 929936acb4..dc652e20a4 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -71,21 +71,6 @@ namespace osu.Game.Overlays.Settings.Sections Action = () => skinEditor?.Toggle(), }, new ExportSkinButton(), - new SettingsCheckbox - { - LabelText = SkinSettingsStrings.BeatmapSkins, - Current = config.GetBindable(OsuSetting.BeatmapSkins) - }, - new SettingsCheckbox - { - LabelText = SkinSettingsStrings.BeatmapColours, - Current = config.GetBindable(OsuSetting.BeatmapColours) - }, - new SettingsCheckbox - { - LabelText = SkinSettingsStrings.BeatmapHitsounds, - Current = config.GetBindable(OsuSetting.BeatmapHitsounds) - }, }; managerUpdated = skins.ItemUpdated.GetBoundCopy(); From c4347de57ef3d9e84f6cf20058a2d8c6f4b5e2a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 14:31:08 +0900 Subject: [PATCH 353/391] Move ruleset settings to own section --- .../Localisation/RulesetSettingsStrings.cs | 19 ++++++++ .../Settings/Sections/GameplaySection.cs | 26 +---------- .../Settings/Sections/RulesetSection.cs | 44 +++++++++++++++++++ osu.Game/Overlays/SettingsOverlay.cs | 1 + 4 files changed, 65 insertions(+), 25 deletions(-) create mode 100644 osu.Game/Localisation/RulesetSettingsStrings.cs create mode 100644 osu.Game/Overlays/Settings/Sections/RulesetSection.cs diff --git a/osu.Game/Localisation/RulesetSettingsStrings.cs b/osu.Game/Localisation/RulesetSettingsStrings.cs new file mode 100644 index 0000000000..a356c9e20b --- /dev/null +++ b/osu.Game/Localisation/RulesetSettingsStrings.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 RulesetSettingsStrings + { + private const string prefix = @"osu.Game.Resources.Localisation.RulesetSettings"; + + /// + /// "Rulesets" + /// + public static LocalisableString Rulesets => new TranslatableString(getKey(@"rulesets"), @"Rulesets"); + + private static string getKey(string key) => $@"{prefix}:{key}"; + } +} diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index c4da641574..995df190bd 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs @@ -1,16 +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.Game.Overlays.Settings.Sections.Gameplay; -using osu.Game.Rulesets; -using System.Linq; using osu.Framework.Graphics.Sprites; -using osu.Framework.Logging; using osu.Framework.Localisation; using osu.Game.Localisation; +using osu.Game.Overlays.Settings.Sections.Gameplay; namespace osu.Game.Overlays.Settings.Sections { @@ -35,24 +30,5 @@ namespace osu.Game.Overlays.Settings.Sections new ModsSettings(), }; } - - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) - { - foreach (Ruleset ruleset in rulesets.AvailableRulesets.Select(info => info.CreateInstance())) - { - try - { - SettingsSubsection section = ruleset.CreateSettings(); - - if (section != null) - Add(section); - } - catch (Exception e) - { - Logger.Error(e, "Failed to load ruleset settings"); - } - } - } } } diff --git a/osu.Game/Overlays/Settings/Sections/RulesetSection.cs b/osu.Game/Overlays/Settings/Sections/RulesetSection.cs new file mode 100644 index 0000000000..f9d15fc821 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/RulesetSection.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; +using osu.Framework.Logging; +using osu.Game.Rulesets; +using osu.Game.Localisation; + +namespace osu.Game.Overlays.Settings.Sections +{ + public class RulesetSection : SettingsSection + { + public override LocalisableString Header => RulesetSettingsStrings.Rulesets; + + public override Drawable CreateIcon() => new SpriteIcon + { + Icon = FontAwesome.Regular.Square + }; + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + foreach (Ruleset ruleset in rulesets.AvailableRulesets.Select(info => info.CreateInstance())) + { + try + { + SettingsSubsection section = ruleset.CreateSettings(); + + if (section != null) + Add(section); + } + catch (Exception e) + { + Logger.Error(e, "Failed to load ruleset settings"); + } + } + } + } +} diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index af91677adb..c84cba8189 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -28,6 +28,7 @@ namespace osu.Game.Overlays new InputSection(createSubPanel(new KeyBindingPanel())), new UserInterfaceSection(), new GameplaySection(), + new RulesetSection(), new AudioSection(), new GraphicsSection(), new OnlineSection(), From 5ca1d1d12ce6591f4e7f1d8a00e8672a01401d42 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 14:34:00 +0900 Subject: [PATCH 354/391] Reorder graphics settings and move gameplay related pieces out --- .../Localisation/GraphicsSettingsStrings.cs | 5 +++ .../Sections/Gameplay/BeatmapSettings.cs | 5 +++ .../Sections/Gameplay/GeneralSettings.cs | 36 +++++++++++++++++++ .../Settings/Sections/Gameplay/HUDSettings.cs | 7 ---- .../Settings/Sections/GameplaySection.cs | 1 + ...etailSettings.cs => ScreenshotSettings.cs} | 14 ++------ .../Settings/Sections/GraphicsSection.cs | 4 +-- 7 files changed, 51 insertions(+), 21 deletions(-) create mode 100644 osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs rename osu.Game/Overlays/Settings/Sections/Graphics/{DetailSettings.cs => ScreenshotSettings.cs} (67%) diff --git a/osu.Game/Localisation/GraphicsSettingsStrings.cs b/osu.Game/Localisation/GraphicsSettingsStrings.cs index 0e384f983f..f85cc0f2ae 100644 --- a/osu.Game/Localisation/GraphicsSettingsStrings.cs +++ b/osu.Game/Localisation/GraphicsSettingsStrings.cs @@ -104,6 +104,11 @@ namespace osu.Game.Localisation /// public static LocalisableString HitLighting => new TranslatableString(getKey(@"hit_lighting"), @"Hit lighting"); + /// + /// "Screenshots" + /// + public static LocalisableString Screenshots => new TranslatableString(getKey(@"screenshots"), @"Screenshots"); + /// /// "Screenshot format" /// diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs index a5d1fc5fc3..aaa60ce81b 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/BeatmapSettings.cs @@ -33,6 +33,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = SkinSettingsStrings.BeatmapHitsounds, Current = config.GetBindable(OsuSetting.BeatmapHitsounds) }, + new SettingsCheckbox + { + LabelText = GraphicsSettingsStrings.StoryboardVideo, + Current = config.GetBindable(OsuSetting.ShowStoryboard) + }, }; } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs new file mode 100644 index 0000000000..d4e4fd571d --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.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.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Localisation; +using osu.Game.Configuration; +using osu.Game.Localisation; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Overlays.Settings.Sections.Gameplay +{ + public class GeneralSettings : SettingsSubsection + { + protected override LocalisableString Header => GameplaySettingsStrings.GeneralHeader; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + Children = new Drawable[] + { + new SettingsEnumDropdown + { + LabelText = GameplaySettingsStrings.ScoreDisplayMode, + Current = config.GetBindable(OsuSetting.ScoreDisplayMode), + Keywords = new[] { "scoring" } + }, + new SettingsCheckbox + { + LabelText = GraphicsSettingsStrings.HitLighting, + Current = config.GetBindable(OsuSetting.HitLighting) + }, + }; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs index 2ffd19a020..e1b452e322 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/HUDSettings.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Configuration; using osu.Game.Localisation; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Overlays.Settings.Sections.Gameplay { @@ -24,12 +23,6 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = GameplaySettingsStrings.HUDVisibilityMode, Current = config.GetBindable(OsuSetting.HUDVisibilityMode) }, - new SettingsEnumDropdown - { - LabelText = GameplaySettingsStrings.ScoreDisplayMode, - Current = config.GetBindable(OsuSetting.ScoreDisplayMode), - Keywords = new[] { "scoring" } - }, new SettingsCheckbox { LabelText = GameplaySettingsStrings.ShowDifficultyGraph, diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index 995df190bd..dd4e561451 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs @@ -22,6 +22,7 @@ namespace osu.Game.Overlays.Settings.Sections { Children = new Drawable[] { + new GeneralSettings(), new AudioSettings(), new BeatmapSettings(), new BackgroundSettings(), diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs similarity index 67% rename from osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs rename to osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs index 20b1d8d801..dbb9ddc1c1 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/ScreenshotSettings.cs @@ -9,25 +9,15 @@ using osu.Game.Localisation; namespace osu.Game.Overlays.Settings.Sections.Graphics { - public class DetailSettings : SettingsSubsection + public class ScreenshotSettings : SettingsSubsection { - protected override LocalisableString Header => GraphicsSettingsStrings.DetailSettingsHeader; + protected override LocalisableString Header => GraphicsSettingsStrings.Screenshots; [BackgroundDependencyLoader] private void load(OsuConfigManager config) { Children = new Drawable[] { - new SettingsCheckbox - { - LabelText = GraphicsSettingsStrings.StoryboardVideo, - Current = config.GetBindable(OsuSetting.ShowStoryboard) - }, - new SettingsCheckbox - { - LabelText = GraphicsSettingsStrings.HitLighting, - Current = config.GetBindable(OsuSetting.HitLighting) - }, new SettingsEnumDropdown { LabelText = GraphicsSettingsStrings.ScreenshotFormat, diff --git a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs index fd0718f9f2..591848506a 100644 --- a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs @@ -22,9 +22,9 @@ namespace osu.Game.Overlays.Settings.Sections { Children = new Drawable[] { - new RendererSettings(), new LayoutSettings(), - new DetailSettings(), + new RendererSettings(), + new ScreenshotSettings(), }; } } From 59202d27c799d3f98a71543a726344c03fd801e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 14:42:22 +0900 Subject: [PATCH 355/391] Add some missing labels --- osu.Game/Configuration/RandomSelectAlgorithm.cs | 2 +- osu.Game/Localisation/AudioSettingsStrings.cs | 5 +++++ osu.Game/Localisation/SkinSettingsStrings.cs | 5 +++++ .../Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs | 1 + osu.Game/Overlays/Settings/Sections/SkinSection.cs | 5 ++++- 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game/Configuration/RandomSelectAlgorithm.cs b/osu.Game/Configuration/RandomSelectAlgorithm.cs index 8d0c87374f..b22f2ae485 100644 --- a/osu.Game/Configuration/RandomSelectAlgorithm.cs +++ b/osu.Game/Configuration/RandomSelectAlgorithm.cs @@ -10,7 +10,7 @@ namespace osu.Game.Configuration [Description("Never repeat")] RandomPermutation, - [Description("Random")] + [Description("True Random")] Random } } diff --git a/osu.Game/Localisation/AudioSettingsStrings.cs b/osu.Game/Localisation/AudioSettingsStrings.cs index aa6eabd7d1..008781c2e5 100644 --- a/osu.Game/Localisation/AudioSettingsStrings.cs +++ b/osu.Game/Localisation/AudioSettingsStrings.cs @@ -24,6 +24,11 @@ namespace osu.Game.Localisation /// public static LocalisableString VolumeHeader => new TranslatableString(getKey(@"volume_header"), @"Volume"); + /// + /// "Output device" + /// + public static LocalisableString OutputDevice => new TranslatableString(getKey(@"output_device"), @"Output device"); + /// /// "Master" /// diff --git a/osu.Game/Localisation/SkinSettingsStrings.cs b/osu.Game/Localisation/SkinSettingsStrings.cs index f22b4d6bf5..8b74b94d59 100644 --- a/osu.Game/Localisation/SkinSettingsStrings.cs +++ b/osu.Game/Localisation/SkinSettingsStrings.cs @@ -14,6 +14,11 @@ namespace osu.Game.Localisation /// public static LocalisableString SkinSectionHeader => new TranslatableString(getKey(@"skin_section_header"), @"Skin"); + /// + /// "Current skin" + /// + public static LocalisableString CurrentSkin => new TranslatableString(getKey(@"current_skin"), @"Current skin"); + /// /// "Skin layout editor" /// diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs index d697b45424..0c54ae2763 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs @@ -28,6 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio { dropdown = new AudioDeviceSettingsDropdown { + LabelText = AudioSettingsStrings.OutputDevice, Keywords = new[] { "speaker", "headphone", "output" } } }; diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index dc652e20a4..00198235c5 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -64,7 +64,10 @@ namespace osu.Game.Overlays.Settings.Sections { Children = new Drawable[] { - skinDropdown = new SkinSettingsDropdown(), + skinDropdown = new SkinSettingsDropdown + { + LabelText = SkinSettingsStrings.CurrentSkin + }, new SettingsButton { Text = SkinSettingsStrings.SkinLayoutEditor, From 24b87cf6552c16f9a49f0d0611d0b037e381ea79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 14:53:19 +0900 Subject: [PATCH 356/391] Change some icons to be more descriptive (still placeholder) --- osu.Game/Overlays/Settings/Sections/GameplaySection.cs | 2 +- osu.Game/Overlays/Settings/Sections/RulesetSection.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index dd4e561451..120e2d908c 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs @@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Regular.Circle + Icon = FontAwesome.Regular.DotCircle }; public GameplaySection() diff --git a/osu.Game/Overlays/Settings/Sections/RulesetSection.cs b/osu.Game/Overlays/Settings/Sections/RulesetSection.cs index f9d15fc821..b9339d5299 100644 --- a/osu.Game/Overlays/Settings/Sections/RulesetSection.cs +++ b/osu.Game/Overlays/Settings/Sections/RulesetSection.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings.Sections public override Drawable CreateIcon() => new SpriteIcon { - Icon = FontAwesome.Regular.Square + Icon = FontAwesome.Solid.Chess }; [BackgroundDependencyLoader] From 129416835f2e7d78d7cbf5b120c7d9708de953f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 15:40:12 +0900 Subject: [PATCH 357/391] Remove stray `string.Empty` specification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Models/RealmBeatmapMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Models/RealmBeatmapMetadata.cs b/osu.Game/Models/RealmBeatmapMetadata.cs index 00dd120791..6ea7170d0f 100644 --- a/osu.Game/Models/RealmBeatmapMetadata.cs +++ b/osu.Game/Models/RealmBeatmapMetadata.cs @@ -26,7 +26,7 @@ namespace osu.Game.Models [JsonProperty("artist_unicode")] public string ArtistUnicode { get; set; } = string.Empty; - public string Author { get; set; } = string.Empty; // eventually should be linked to a persisted User. = string.Empty; + public string Author { get; set; } = string.Empty; // eventually should be linked to a persisted User. public string Source { get; set; } = string.Empty; From ce128476ae59d1011bf17fac4699ddfcb8f8a59f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 15:46:32 +0900 Subject: [PATCH 358/391] Remove public setter of `RealmFileStore.Storage` --- osu.Game/Stores/RealmFileStore.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Stores/RealmFileStore.cs b/osu.Game/Stores/RealmFileStore.cs index aac52b193c..5082b2c65b 100644 --- a/osu.Game/Stores/RealmFileStore.cs +++ b/osu.Game/Stores/RealmFileStore.cs @@ -17,14 +17,15 @@ using Realms; namespace osu.Game.Stores { /// - /// Handles the Store and retrieval of Files/FileSets to the database backing + /// Handles the storing of files to the file system (and database) backing. /// public class RealmFileStore { private readonly RealmContextFactory realmFactory; + public readonly IResourceStore Store; - public Storage Storage; + public readonly Storage Storage; public RealmFileStore(RealmContextFactory realmFactory, Storage storage) { From 0df9ab3eec5d6a3d039d3a0cac5ff62e297dd62f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 16:04:09 +0900 Subject: [PATCH 359/391] Fix migration blocking code running on the wrong thread --- osu.Game/OsuGameBase.cs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 7f4fe8a943..09eb482d16 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; @@ -410,11 +411,28 @@ namespace osu.Game { Logger.Log($@"Migrating osu! data from ""{Storage.GetFullPath(string.Empty)}"" to ""{path}""..."); - using (realmFactory.BlockAllOperations()) + IDisposable realmBlocker = null; + + try { - contextFactory.FlushConnections(); + ManualResetEventSlim readyToRun = new ManualResetEventSlim(); + + Scheduler.Add(() => + { + realmBlocker = realmFactory.BlockAllOperations(); + contextFactory.FlushConnections(); + + readyToRun.Set(); + }, false); + + readyToRun.Wait(); + (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); } + finally + { + realmBlocker?.Dispose(); + } Logger.Log(@"Migration complete!"); } From 76c64751de3007a21f6092359c60e4dbae4f0aaa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 16:18:12 +0900 Subject: [PATCH 360/391] Remove `RealmBeatmap.Clone` for the time being (incorrectly implemented) --- osu.Game/Models/RealmBeatmap.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Models/RealmBeatmap.cs b/osu.Game/Models/RealmBeatmap.cs index 09f8dafeb6..5049c1384d 100644 --- a/osu.Game/Models/RealmBeatmap.cs +++ b/osu.Game/Models/RealmBeatmap.cs @@ -97,11 +97,6 @@ namespace osu.Game.Models #endregion - /// - /// Returns a shallow-clone of this . - /// - public RealmBeatmap Clone() => (RealmBeatmap)MemberwiseClone(); - public bool AudioEquals(RealmBeatmap? other) => other != null && BeatmapSet != null && other.BeatmapSet != null From c66e50ac55147889526c3bfcc4109c9ba8eca339 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 16:19:58 +0900 Subject: [PATCH 361/391] Remove temporary logging --- osu.Game/Audio/Effects/AudioFilter.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/osu.Game/Audio/Effects/AudioFilter.cs b/osu.Game/Audio/Effects/AudioFilter.cs index 5eaa87af8d..268c31aa4c 100644 --- a/osu.Game/Audio/Effects/AudioFilter.cs +++ b/osu.Game/Audio/Effects/AudioFilter.cs @@ -132,16 +132,8 @@ namespace osu.Game.Audio.Effects { base.Dispose(isDisposing); - try - { - if (mixer.Effects.Contains(filter)) - detachFilter(); - } - catch (Exception e) - { - Logger.Log($"Exception in audio filter disposal: {e}"); - throw; - } + if (mixer.Effects.Contains(filter)) + detachFilter(); } } } From 6fec821a17d24b0462e42ebba7d64eaa5fd520a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 16:20:13 +0900 Subject: [PATCH 362/391] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index fefc2f6438..956093b2ac 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ff382f5227..184c9d3f63 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index fff0cbf418..38b920420b 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From 55bd7d25126f0441a1367188e80e7f17d606f200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 9 Oct 2021 16:12:08 +0200 Subject: [PATCH 363/391] Add failing coverage for saving difficulty params from editor --- osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index 2258a209e2..7540a0c69a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -32,6 +32,8 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("wait for editor load", () => editor != null); + AddStep("Set overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty = 7); + AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); AddStep("Enter compose mode", () => InputManager.Key(Key.F1)); @@ -57,6 +59,7 @@ namespace osu.Game.Tests.Visual.Editing AddUntilStep("Wait for editor load", () => editor != null); AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1); + AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7); } } } From b79cf0b58b3e5c3a136e912ab4b68dab2e3eec9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 9 Oct 2021 15:00:47 +0200 Subject: [PATCH 364/391] Add failing coverage for conversion not altering original beatmap --- osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index 64f1ee4a7a..6d63525011 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using osu.Game.IO; +using osu.Game.IO.Serialization; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -109,6 +110,8 @@ namespace osu.Game.Tests.Beatmaps { var beatmap = GetBeatmap(name); + string beforeConversion = beatmap.Serialize(); + var converterResult = new Dictionary>(); var working = new ConversionWorkingBeatmap(beatmap) @@ -122,6 +125,10 @@ namespace osu.Game.Tests.Beatmaps working.GetPlayableBeatmap(CreateRuleset().RulesetInfo, mods); + string afterConversion = beatmap.Serialize(); + + Assert.AreEqual(beforeConversion, afterConversion, "Conversion altered original beatmap"); + return new ConvertResult { Mappings = converterResult.Select(r => From 1373cc02d7293a1d9884de1c791cad513d57bb4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 16:42:09 +0900 Subject: [PATCH 365/391] Shallow clone `BeatmapInfo` during conversion process to avoid overwriting fields --- osu.Game/Beatmaps/BeatmapConverter.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index f3434c5153..627e54c803 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -40,7 +40,13 @@ namespace osu.Game.Beatmaps public IBeatmap Convert(CancellationToken cancellationToken = default) { // We always operate on a clone of the original beatmap, to not modify it game-wide - return ConvertBeatmap(Beatmap.Clone(), cancellationToken); + var original = Beatmap.Clone(); + + // Shallow clone isn't enough to ensure we don't mutate beatmap info unexpectedly. + // Can potentially be removed after `Beatmap.Difficulty` doesn't save back to `Beatmap.BeatmapInfo`. + original.BeatmapInfo = original.BeatmapInfo.Clone(); + + return ConvertBeatmap(original, cancellationToken); } /// From d1b0dd1f6b72112aa10c18f9106b6b08a14b8d48 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 12 Oct 2021 17:13:36 +0900 Subject: [PATCH 366/391] Fix GameHosts being run on TPL threads --- osu.Game.Tests/ImportTest.cs | 2 +- osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/ImportTest.cs b/osu.Game.Tests/ImportTest.cs index e888f51e98..dbeb453d4d 100644 --- a/osu.Game.Tests/ImportTest.cs +++ b/osu.Game.Tests/ImportTest.cs @@ -17,7 +17,7 @@ namespace osu.Game.Tests protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false) { var osu = new TestOsuGameBase(withBeatmap); - Task.Run(() => host.Run(osu)) + Task.Factory.StartNew(() => host.Run(osu), TaskCreationOptions.LongRunning) .ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted); waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); diff --git a/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs b/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs index b14684200f..319a768e65 100644 --- a/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/TournamentHostTest.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tournament.Tests.NonVisual public static TournamentGameBase LoadTournament(GameHost host, TournamentGameBase tournament = null) { tournament ??= new TournamentGameBase(); - Task.Run(() => host.Run(tournament)) + Task.Factory.StartNew(() => host.Run(tournament), TaskCreationOptions.LongRunning) .ContinueWith(t => Assert.Fail($"Host threw exception {t.Exception}"), TaskContinuationOptions.OnlyOnFaulted); WaitForOrAssert(() => tournament.IsLoaded, @"osu! failed to start in a reasonable amount of time"); return tournament; From e6cd0a837173ecfe18c79e7c4045b44fd5591b87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 17:17:40 +0900 Subject: [PATCH 367/391] Remove unused using statements --- osu.Game/Audio/Effects/AudioFilter.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Audio/Effects/AudioFilter.cs b/osu.Game/Audio/Effects/AudioFilter.cs index 268c31aa4c..ee48bdd7d9 100644 --- a/osu.Game/Audio/Effects/AudioFilter.cs +++ b/osu.Game/Audio/Effects/AudioFilter.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.Diagnostics; using ManagedBass.Fx; using osu.Framework.Audio.Mixing; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Logging; namespace osu.Game.Audio.Effects { From 79ac64a0885620f9755fb5cc82cb226c34cb21c5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Oct 2021 19:40:35 +0900 Subject: [PATCH 368/391] Split out editor save steps to try and catch test failure --- osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index 2258a209e2..f4e704b22b 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -41,11 +41,11 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre)); AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left)); - AddStep("Save and exit", () => - { - InputManager.Keys(PlatformAction.Save); - InputManager.Key(Key.Escape); - }); + AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1); + + AddStep("Save", () => InputManager.Keys(PlatformAction.Save)); + + AddStep("Exit", () => InputManager.Key(Key.Escape)); AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu); From 91c286b1ade4e8ff9941f972dd5718f9544b0ceb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 12 Oct 2021 22:07:57 +0900 Subject: [PATCH 369/391] Fix intermittent TestScenePlaySongSelect test failures --- .../Visual/SongSelect/TestScenePlaySongSelect.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 067f1cabb4..4811fc979e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -142,6 +142,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("store selected beatmap", () => selected = Beatmap.Value); + AddUntilStep("wait for beatmaps to load", () => songSelect.Carousel.ChildrenOfType().Any()); + AddStep("select next and enter", () => { InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType() @@ -599,10 +601,10 @@ namespace osu.Game.Tests.Visual.SongSelect }); FilterableDifficultyIcon difficultyIcon = null; - AddStep("Find an icon", () => + AddUntilStep("Find an icon", () => { - difficultyIcon = set.ChildrenOfType() - .First(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex()); + return (difficultyIcon = set.ChildrenOfType() + .FirstOrDefault(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex())) != null; }); AddStep("Click on a difficulty", () => @@ -765,10 +767,10 @@ namespace osu.Game.Tests.Visual.SongSelect }); FilterableGroupedDifficultyIcon groupIcon = null; - AddStep("Find group icon for different ruleset", () => + AddUntilStep("Find group icon for different ruleset", () => { - groupIcon = set.ChildrenOfType() - .First(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3); + return (groupIcon = set.ChildrenOfType() + .FirstOrDefault(icon => icon.Items.First().BeatmapInfo.Ruleset.ID == 3)) != null; }); AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); From 8dcfc3dd7e2608ef1bc715a546dab144e1d9fab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 12 Oct 2021 18:31:20 +0200 Subject: [PATCH 370/391] Replace no-op seeks with wait steps --- .../Visual/Gameplay/TestSceneFrameStabilityContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs index 881e3f097c..ae0decaee1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFrameStabilityContainer.cs @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestSeekToSameTimePreservesRate() + public void TestRatePreservedWhenTimeNotProgressing() { AddStep("set manual clock rate", () => manualClock.Rate = 1); seekManualTo(5000); @@ -114,13 +114,13 @@ namespace osu.Game.Tests.Visual.Gameplay seekManualTo(10000); checkRate(1); - seekManualTo(10000); + AddWaitStep("wait some", 3); checkRate(1); seekManualTo(5000); checkRate(-1); - seekManualTo(5000); + AddWaitStep("wait some", 3); checkRate(-1); seekManualTo(10000); From 94de24075e52d154ca042a8bf9116f5f434d8e23 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Oct 2021 12:18:56 +0900 Subject: [PATCH 371/391] Ensure startup imports trigger notifications --- osu.Game/OsuGame.cs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 7895715045..020cdebab6 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -211,13 +211,6 @@ namespace osu.Game [BackgroundDependencyLoader] private void load() { - if (args?.Length > 0) - { - var paths = args.Where(a => !a.StartsWith('-')).ToArray(); - if (paths.Length > 0) - Task.Run(() => Import(paths)); - } - dependencies.CacheAs(this); dependencies.Cache(SentryLogger); @@ -867,6 +860,19 @@ namespace osu.Game { if (mode.NewValue != OverlayActivation.All) CloseAllOverlays(); }; + + // Importantly, this should be run after binding PostNotification to the import handlers so they can present the import after game startup. + handleStartupImport(); + } + + private void handleStartupImport() + { + if (args?.Length > 0) + { + var paths = args.Where(a => !a.StartsWith('-')).ToArray(); + if (paths.Length > 0) + Task.Run(() => Import(paths)); + } } private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays) From f69a56a26ac5debb0e365332e9f623fedb5fa937 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Oct 2021 12:19:10 +0900 Subject: [PATCH 372/391] Add test coverage of startup import sequence --- .../Navigation/TestSceneStartupImport.cs | 31 +++++++++++++++++++ osu.Game/Database/ArchiveModelManager.cs | 4 +-- .../Database/ImportProgressNotification.cs | 15 +++++++++ osu.Game/Tests/Visual/OsuGameTestScene.cs | 7 +++-- 4 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs create mode 100644 osu.Game/Database/ImportProgressNotification.cs diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs new file mode 100644 index 0000000000..cb7c334656 --- /dev/null +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.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 System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Database; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Visual.Navigation +{ + public class TestSceneStartupImport : OsuGameTestScene + { + private string importFilename; + + protected override TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API, new[] { importFilename }); + + public override void SetUpSteps() + { + AddStep("Prepare import beatmap", () => importFilename = TestResources.GetTestBeatmapForImport()); + + base.SetUpSteps(); + } + + [Test] + public void TestImportCreatedNotification() + { + AddUntilStep("Import notification was presented", () => Game.Notifications.ChildrenOfType().Count() == 1); + } + } +} diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index ee1a7e2900..c235fc7728 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -116,7 +116,7 @@ namespace osu.Game.Database /// One or more archive locations on disk. public Task Import(params string[] paths) { - var notification = new ProgressNotification { State = ProgressNotificationState.Active }; + var notification = new ImportProgressNotification(); PostNotification?.Invoke(notification); @@ -125,7 +125,7 @@ namespace osu.Game.Database public Task Import(params ImportTask[] tasks) { - var notification = new ProgressNotification { State = ProgressNotificationState.Active }; + var notification = new ImportProgressNotification(); PostNotification?.Invoke(notification); diff --git a/osu.Game/Database/ImportProgressNotification.cs b/osu.Game/Database/ImportProgressNotification.cs new file mode 100644 index 0000000000..aaee3e117f --- /dev/null +++ b/osu.Game/Database/ImportProgressNotification.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.Overlays.Notifications; + +namespace osu.Game.Database +{ + public class ImportProgressNotification : ProgressNotification + { + public ImportProgressNotification() + { + State = ProgressNotificationState.Active; + } + } +} diff --git a/osu.Game/Tests/Visual/OsuGameTestScene.cs b/osu.Game/Tests/Visual/OsuGameTestScene.cs index 77db697cb6..6a11bd3fea 100644 --- a/osu.Game/Tests/Visual/OsuGameTestScene.cs +++ b/osu.Game/Tests/Visual/OsuGameTestScene.cs @@ -78,9 +78,11 @@ namespace osu.Game.Tests.Visual protected void CreateGame() { - AddGame(Game = new TestOsuGame(LocalStorage, API)); + AddGame(Game = CreateTestGame()); } + protected virtual TestOsuGame CreateTestGame() => new TestOsuGame(LocalStorage, API); + protected void PushAndConfirm(Func newScreen) { Screen screen = null; @@ -135,7 +137,8 @@ namespace osu.Game.Tests.Visual public new void PerformFromScreen(Action action, IEnumerable validScreens = null) => base.PerformFromScreen(action, validScreens); - public TestOsuGame(Storage storage, IAPIProvider api) + public TestOsuGame(Storage storage, IAPIProvider api, string[] args = null) + : base(args) { Storage = storage; API = api; From cf10239e70408533a0138f9afb5a45ee4e615e99 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 13 Oct 2021 12:51:41 +0900 Subject: [PATCH 373/391] Add a few nullabilities and DCC excludes --- osu.Game/Database/IHasRealmFiles.cs | 2 ++ osu.Game/Database/INamedFile.cs | 8 +++++--- osu.Game/Models/RealmBeatmapDifficulty.cs | 2 ++ osu.Game/Models/RealmFile.cs | 2 ++ osu.Game/Models/RealmNamedFileUsage.cs | 2 ++ osu.Game/Stores/RealmFileStore.cs | 2 ++ 6 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/IHasRealmFiles.cs b/osu.Game/Database/IHasRealmFiles.cs index 2adfe73d1e..024d9f2a89 100644 --- a/osu.Game/Database/IHasRealmFiles.cs +++ b/osu.Game/Database/IHasRealmFiles.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using osu.Game.Models; +#nullable enable + namespace osu.Game.Database { /// diff --git a/osu.Game/Database/INamedFile.cs b/osu.Game/Database/INamedFile.cs index 9c94aed38c..2bd45d4e42 100644 --- a/osu.Game/Database/INamedFile.cs +++ b/osu.Game/Database/INamedFile.cs @@ -3,15 +3,17 @@ using osu.Game.Models; +#nullable enable + namespace osu.Game.Database { /// - /// Represent a join model which gives a filename and scope to a . + /// Represents a join model which gives a filename and scope to a . /// public interface INamedFile { - public string Filename { get; set; } + string Filename { get; set; } - public RealmFile File { get; set; } + RealmFile File { get; set; } } } diff --git a/osu.Game/Models/RealmBeatmapDifficulty.cs b/osu.Game/Models/RealmBeatmapDifficulty.cs index 44bfdda491..3c1dad69e4 100644 --- a/osu.Game/Models/RealmBeatmapDifficulty.cs +++ b/osu.Game/Models/RealmBeatmapDifficulty.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.Testing; using osu.Game.Beatmaps; using Realms; @@ -8,6 +9,7 @@ using Realms; namespace osu.Game.Models { + [ExcludeFromDynamicCompile] [MapTo("BeatmapDifficulty")] public class RealmBeatmapDifficulty : EmbeddedObject, IBeatmapDifficultyInfo { diff --git a/osu.Game/Models/RealmFile.cs b/osu.Game/Models/RealmFile.cs index 6836d79d2d..2715f4be45 100644 --- a/osu.Game/Models/RealmFile.cs +++ b/osu.Game/Models/RealmFile.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.IO; +using osu.Framework.Testing; using osu.Game.IO; using Realms; @@ -9,6 +10,7 @@ using Realms; namespace osu.Game.Models { + [ExcludeFromDynamicCompile] [MapTo("File")] public class RealmFile : RealmObject, IFileInfo { diff --git a/osu.Game/Models/RealmNamedFileUsage.cs b/osu.Game/Models/RealmNamedFileUsage.cs index 59b446112d..ba12d51d0b 100644 --- a/osu.Game/Models/RealmNamedFileUsage.cs +++ b/osu.Game/Models/RealmNamedFileUsage.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using JetBrains.Annotations; +using osu.Framework.Testing; using osu.Game.Database; using osu.Game.IO; using Realms; @@ -10,6 +11,7 @@ using Realms; namespace osu.Game.Models { + [ExcludeFromDynamicCompile] public class RealmNamedFileUsage : EmbeddedObject, INamedFile, INamedFileUsage { public RealmFile File { get; set; } = null!; diff --git a/osu.Game/Stores/RealmFileStore.cs b/osu.Game/Stores/RealmFileStore.cs index 5082b2c65b..f7b7471634 100644 --- a/osu.Game/Stores/RealmFileStore.cs +++ b/osu.Game/Stores/RealmFileStore.cs @@ -8,6 +8,7 @@ using osu.Framework.Extensions; using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Models; using Realms; @@ -19,6 +20,7 @@ namespace osu.Game.Stores /// /// Handles the storing of files to the file system (and database) backing. /// + [ExcludeFromDynamicCompile] public class RealmFileStore { private readonly RealmContextFactory realmFactory; From b37096f44062fbff4b4dd9e23708af55ffb4f249 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Oct 2021 13:25:30 +0900 Subject: [PATCH 374/391] Avoid using bindable for `AudioFilter` cutoff It doesn't play nicely with screen exiting, as it is automatically unbound during the exit process. Easiest to just avoid using this for now. --- .../Visual/Audio/TestSceneAudioFilter.cs | 27 +++++++--- osu.Game/Audio/Effects/AudioFilter.cs | 51 ++++++++++--------- .../Audio/Effects/ITransformableFilter.cs | 7 ++- 3 files changed, 50 insertions(+), 35 deletions(-) diff --git a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs index 211543a881..851e0eb2a1 100644 --- a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs +++ b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs @@ -34,6 +34,9 @@ namespace osu.Game.Tests.Visual.Audio beatmap = new WaveformTestBeatmap(audio); track = beatmap.LoadTrack(); + OsuSliderBar lowPassCutoff; + OsuSliderBar highPassCutoff; + Add(new FillFlowContainer { Children = new Drawable[] @@ -43,33 +46,41 @@ namespace osu.Game.Tests.Visual.Audio lowpassText = new OsuSpriteText { Padding = new MarginPadding(20), - Text = $"Low Pass: {lowpassFilter.Cutoff.Value}hz", + Text = $"Low Pass: {lowpassFilter.Cutoff}hz", Font = new FontUsage(size: 40) }, - new OsuSliderBar + lowPassCutoff = new OsuSliderBar { Width = 500, Height = 50, Padding = new MarginPadding(20), - Current = { BindTarget = lowpassFilter.Cutoff } }, highpassText = new OsuSpriteText { Padding = new MarginPadding(20), - Text = $"High Pass: {highpassFilter.Cutoff.Value}hz", + Text = $"High Pass: {highpassFilter.Cutoff}hz", Font = new FontUsage(size: 40) }, - new OsuSliderBar + highPassCutoff = new OsuSliderBar { Width = 500, Height = 50, Padding = new MarginPadding(20), - Current = { BindTarget = highpassFilter.Cutoff } } } }); - lowpassFilter.Cutoff.ValueChanged += e => lowpassText.Text = $"Low Pass: {e.NewValue}hz"; - highpassFilter.Cutoff.ValueChanged += e => highpassText.Text = $"High Pass: {e.NewValue}hz"; + + lowPassCutoff.Current.ValueChanged += e => + { + lowpassText.Text = $"Low Pass: {e.NewValue}hz"; + lowpassFilter.Cutoff = e.NewValue; + }; + + highPassCutoff.Current.ValueChanged += e => + { + highpassText.Text = $"High Pass: {e.NewValue}hz"; + highpassFilter.Cutoff = e.NewValue; + }; } [SetUpSteps] diff --git a/osu.Game/Audio/Effects/AudioFilter.cs b/osu.Game/Audio/Effects/AudioFilter.cs index ee48bdd7d9..0152254945 100644 --- a/osu.Game/Audio/Effects/AudioFilter.cs +++ b/osu.Game/Audio/Effects/AudioFilter.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using ManagedBass.Fx; using osu.Framework.Audio.Mixing; -using osu.Framework.Bindables; using osu.Framework.Graphics; namespace osu.Game.Audio.Effects @@ -21,10 +20,25 @@ namespace osu.Game.Audio.Effects private readonly BQFParameters filter; private readonly BQFType type; + private int cutoff; + /// - /// The current cutoff of this filter. + /// The cutoff frequency of this filter. /// - public BindableNumber Cutoff { get; } + public int Cutoff + { + get => cutoff; + set + { + if (value == cutoff) + return; + + int oldValue = cutoff; + cutoff = value; + + updateFilter(oldValue, cutoff); + } + } /// /// A Component that implements a BASS FX BiQuad Filter Effect. @@ -36,33 +50,25 @@ namespace osu.Game.Audio.Effects this.mixer = mixer; this.type = type; - int initialCutoff; - switch (type) { case BQFType.HighPass: - initialCutoff = 1; + cutoff = 1; break; case BQFType.LowPass: - initialCutoff = MAX_LOWPASS_CUTOFF; + cutoff = MAX_LOWPASS_CUTOFF; break; default: - initialCutoff = 500; // A default that should ensure audio remains audible for other filters. + cutoff = 500; // A default that should ensure audio remains audible for other filters. break; } - Cutoff = new BindableNumber(initialCutoff) - { - MinValue = 1, - MaxValue = MAX_LOWPASS_CUTOFF - }; - filter = new BQFParameters { lFilter = type, - fCenter = initialCutoff, + fCenter = cutoff, fBandwidth = 0, fQ = 0.7f // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0) }; @@ -70,8 +76,6 @@ namespace osu.Game.Audio.Effects // Don't start attached if this is low-pass or high-pass filter (as they have special auto-attach/detach logic) if (type != BQFType.LowPass && type != BQFType.HighPass) attachFilter(); - - Cutoff.ValueChanged += updateFilter; } private void attachFilter() @@ -86,40 +90,41 @@ namespace osu.Game.Audio.Effects mixer.Effects.Remove(filter); } - private void updateFilter(ValueChangedEvent cutoff) + private void updateFilter(int oldValue, int newValue) { // Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz. if (type == BQFType.LowPass) { - if (cutoff.NewValue >= MAX_LOWPASS_CUTOFF) + if (newValue >= MAX_LOWPASS_CUTOFF) { detachFilter(); return; } - if (cutoff.OldValue >= MAX_LOWPASS_CUTOFF && cutoff.NewValue < MAX_LOWPASS_CUTOFF) + if (oldValue >= MAX_LOWPASS_CUTOFF) attachFilter(); } // Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz. if (type == BQFType.HighPass) { - if (cutoff.NewValue <= 1) + if (newValue <= 1) { detachFilter(); return; } - if (cutoff.OldValue <= 1 && cutoff.NewValue > 1) + if (oldValue <= 1) attachFilter(); } var filterIndex = mixer.Effects.IndexOf(filter); + if (filterIndex < 0) return; if (mixer.Effects[filterIndex] is BQFParameters existingFilter) { - existingFilter.fCenter = cutoff.NewValue; + existingFilter.fCenter = newValue; // required to update effect with new parameters. mixer.Effects[filterIndex] = existingFilter; diff --git a/osu.Game/Audio/Effects/ITransformableFilter.cs b/osu.Game/Audio/Effects/ITransformableFilter.cs index e4de4cf8ff..fb6a924f68 100644 --- a/osu.Game/Audio/Effects/ITransformableFilter.cs +++ b/osu.Game/Audio/Effects/ITransformableFilter.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.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; @@ -12,7 +11,7 @@ namespace osu.Game.Audio.Effects /// /// The filter cutoff. /// - BindableNumber Cutoff { get; } + int Cutoff { get; set; } } public static class FilterableAudioComponentExtensions @@ -40,7 +39,7 @@ namespace osu.Game.Audio.Effects public static TransformSequence CutoffTo(this T component, int newCutoff, double duration, TEasing easing) where T : class, ITransformableFilter, IDrawable where TEasing : IEasingFunction - => component.TransformBindableTo(component.Cutoff, newCutoff, duration, easing); + => component.TransformTo(nameof(component.Cutoff), newCutoff, duration, easing); /// /// Smoothly adjusts filter cutoff over time. @@ -49,6 +48,6 @@ namespace osu.Game.Audio.Effects public static TransformSequence CutoffTo(this TransformSequence sequence, int newCutoff, double duration, TEasing easing) where T : class, ITransformableFilter, IDrawable where TEasing : IEasingFunction - => sequence.Append(o => o.TransformBindableTo(o.Cutoff, newCutoff, duration, easing)); + => sequence.Append(o => o.TransformTo(nameof(o.Cutoff), newCutoff, duration, easing)); } } From ae4dcbd8297b2803ca1df71c33722118a4a99134 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Oct 2021 13:26:20 +0900 Subject: [PATCH 375/391] Improve `PlayerLoader` audio and visual transitions --- osu.Game/Screens/Play/PlayerLoader.cs | 71 ++++++++++++++++----------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 94a61a4ef3..cf5bff57cf 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -35,6 +35,8 @@ namespace osu.Game.Screens.Play { protected const float BACKGROUND_BLUR = 15; + private const double content_out_duration = 300; + public override bool HideOverlaysOnEnter => hideOverlays; public override bool DisallowExternalBeatmapRulesetChanges => true; @@ -135,36 +137,39 @@ namespace osu.Game.Screens.Play muteWarningShownOnce = sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce); batteryWarningShownOnce = sessionStatics.GetBindable(Static.LowBatteryNotificationShownOnce); - InternalChild = (content = new LogoTrackingContainer + InternalChildren = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - }).WithChildren(new Drawable[] - { - MetadataInfo = new BeatmapMetadataDisplay(Beatmap.Value, Mods, content.LogoFacade) + (content = new LogoTrackingContainer { - Alpha = 0, Anchor = Anchor.Centre, Origin = Anchor.Centre, - }, - PlayerSettings = new FillFlowContainer + RelativeSizeAxes = Axes.Both, + }).WithChildren(new Drawable[] { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 20), - Margin = new MarginPadding(25), - Children = new PlayerSettingsGroup[] + MetadataInfo = new BeatmapMetadataDisplay(Beatmap.Value, Mods, content.LogoFacade) { - VisualSettings = new VisualSettings(), - new InputSettings() - } - }, - idleTracker = new IdleTracker(750), + Alpha = 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + PlayerSettings = new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Margin = new MarginPadding(25), + Children = new PlayerSettingsGroup[] + { + VisualSettings = new VisualSettings(), + new InputSettings() + } + }, + idleTracker = new IdleTracker(750), + }), lowPassFilter = new AudioFilter(audio.TrackMixer) - }); + }; if (Beatmap.Value.BeatmapInfo.EpilepsyWarning) { @@ -195,7 +200,6 @@ namespace osu.Game.Screens.Play epilepsyWarning.DimmableBackground = b; }); - lowPassFilter.CutoffTo(500, 100, Easing.OutCubic); Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); content.ScaleTo(0.7f); @@ -240,15 +244,15 @@ namespace osu.Game.Screens.Play public override bool OnExiting(IScreen next) { cancelLoad(); + contentOut(); - content.ScaleTo(0.7f, 150, Easing.InQuint); - this.FadeOut(150); + // Ensure the screen doesn't expire until all the outwards fade operations have completed. + this.Delay(content_out_duration).FadeOut(); ApplyToBackground(b => b.IgnoreUserSettings.Value = true); BackgroundBrightnessReduction = false; Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); - lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic); return base.OnExiting(next); } @@ -344,6 +348,7 @@ namespace osu.Game.Screens.Play content.FadeInFromZero(400); content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer); + lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint); ApplyToBackground(b => b?.FadeColour(Color4.White, 800, Easing.OutQuint)); } @@ -353,8 +358,9 @@ namespace osu.Game.Screens.Play // Ensure the logo is no longer tracking before we scale the content content.StopTracking(); - content.ScaleTo(0.7f, 300, Easing.InQuint); - content.FadeOut(250); + content.ScaleTo(0.7f, content_out_duration * 2, Easing.OutQuint); + content.FadeOut(content_out_duration, Easing.OutQuint); + lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, content_out_duration); } private void pushWhenLoaded() @@ -381,7 +387,7 @@ namespace osu.Game.Screens.Play contentOut(); - TransformSequence pushSequence = this.Delay(250); + TransformSequence pushSequence = this.Delay(content_out_duration); // 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). @@ -400,6 +406,11 @@ namespace osu.Game.Screens.Play }) .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); + } pushSequence.Schedule(() => { From 26a1e40d2471a9d4e7e44ea0f98b1b1fbc6231c5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Oct 2021 13:47:49 +0900 Subject: [PATCH 376/391] Fix storyboard outro during fail test not being lenient enough --- .../Visual/Gameplay/TestSceneStoryboardWithOutro.cs | 6 +++++- osu.Game/Screens/Play/FailAnimation.cs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs index 3ed274690e..48a97d54f7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardWithOutro.cs @@ -90,8 +90,12 @@ namespace osu.Game.Tests.Visual.Gameplay CreateTest(() => { AddStep("fail on first judgement", () => currentFailConditions = (_, __) => true); - AddStep("set storyboard duration to 1.3s", () => currentStoryboardDuration = 1300); + + // Fail occurs at 164ms with the provided beatmap. + // Fail animation runs for 2.5s realtime but the gameplay time change is *variable* due to the frequency transform being applied, so we need a bit of lenience. + AddStep("set storyboard duration to 0.6s", () => currentStoryboardDuration = 600); }); + AddUntilStep("wait for fail", () => Player.HasFailed); AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration); AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible); diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index e250791b72..ea158c5789 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Play { /// /// Manage the animation to be applied when a player fails. - /// Single file; automatically disposed after use. + /// Single use and automatically disposed after use. /// public class FailAnimation : CompositeDrawable { From 29dfe33465aa61ba7b7866b4e136b7b11ce0fe84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Oct 2021 15:13:35 +0900 Subject: [PATCH 377/391] Rewrite `AudioFilter` to be easier to follow (and fix tests) --- .../Visual/Audio/TestSceneAudioFilter.cs | 83 ++++++++---- osu.Game/Audio/Effects/AudioFilter.cs | 123 +++++++++--------- 2 files changed, 119 insertions(+), 87 deletions(-) diff --git a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs index 851e0eb2a1..0107632f6e 100644 --- a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs +++ b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -18,84 +19,112 @@ namespace osu.Game.Tests.Visual.Audio { public class TestSceneAudioFilter : OsuTestScene { - private OsuSpriteText lowpassText; - private AudioFilter lowpassFilter; + private OsuSpriteText lowPassText; + private AudioFilter lowPassFilter; - private OsuSpriteText highpassText; - private AudioFilter highpassFilter; + private OsuSpriteText highPassText; + private AudioFilter highPassFilter; private Track track; private WaveformTestBeatmap beatmap; + private OsuSliderBar lowPassSlider; + private OsuSliderBar highPassSlider; + [BackgroundDependencyLoader] private void load(AudioManager audio) { beatmap = new WaveformTestBeatmap(audio); track = beatmap.LoadTrack(); - OsuSliderBar lowPassCutoff; - OsuSliderBar highPassCutoff; - Add(new FillFlowContainer { Children = new Drawable[] { - lowpassFilter = new AudioFilter(audio.TrackMixer), - highpassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass), - lowpassText = new OsuSpriteText + lowPassFilter = new AudioFilter(audio.TrackMixer), + highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass), + lowPassText = new OsuSpriteText { Padding = new MarginPadding(20), - Text = $"Low Pass: {lowpassFilter.Cutoff}hz", + Text = $"Low Pass: {lowPassFilter.Cutoff}hz", Font = new FontUsage(size: 40) }, - lowPassCutoff = new OsuSliderBar + lowPassSlider = new OsuSliderBar { Width = 500, Height = 50, Padding = new MarginPadding(20), + Current = new BindableInt + { + MinValue = 0, + MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF, + } }, - highpassText = new OsuSpriteText + highPassText = new OsuSpriteText { Padding = new MarginPadding(20), - Text = $"High Pass: {highpassFilter.Cutoff}hz", + Text = $"High Pass: {highPassFilter.Cutoff}hz", Font = new FontUsage(size: 40) }, - highPassCutoff = new OsuSliderBar + highPassSlider = new OsuSliderBar { Width = 500, Height = 50, Padding = new MarginPadding(20), + Current = new BindableInt + { + MinValue = 0, + MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF, + } } } }); - lowPassCutoff.Current.ValueChanged += e => + lowPassSlider.Current.ValueChanged += e => { - lowpassText.Text = $"Low Pass: {e.NewValue}hz"; - lowpassFilter.Cutoff = e.NewValue; + lowPassText.Text = $"Low Pass: {e.NewValue}hz"; + lowPassFilter.Cutoff = e.NewValue; }; - highPassCutoff.Current.ValueChanged += e => + highPassSlider.Current.ValueChanged += e => { - highpassText.Text = $"High Pass: {e.NewValue}hz"; - highpassFilter.Cutoff = e.NewValue; + highPassText.Text = $"High Pass: {e.NewValue}hz"; + highPassFilter.Cutoff = e.NewValue; }; } + #region Overrides of Drawable + + protected override void Update() + { + base.Update(); + highPassSlider.Current.Value = highPassFilter.Cutoff; + lowPassSlider.Current.Value = lowPassFilter.Cutoff; + } + + #endregion + [SetUpSteps] public void SetUpSteps() { AddStep("Play Track", () => track.Start()); + + AddStep("Reset filters", () => + { + lowPassFilter.Cutoff = AudioFilter.MAX_LOWPASS_CUTOFF; + highPassFilter.Cutoff = 0; + }); + waitTrackPlay(); } [Test] - public void TestLowPass() + public void TestLowPassSweep() { AddStep("Filter Sweep", () => { - lowpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then() + lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then() .CutoffTo(0, 2000, Easing.OutCubic); }); @@ -103,7 +132,7 @@ namespace osu.Game.Tests.Visual.Audio AddStep("Filter Sweep (reverse)", () => { - lowpassFilter.CutoffTo(0).Then() + lowPassFilter.CutoffTo(0).Then() .CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic); }); @@ -112,11 +141,11 @@ namespace osu.Game.Tests.Visual.Audio } [Test] - public void TestHighPass() + public void TestHighPassSweep() { AddStep("Filter Sweep", () => { - highpassFilter.CutoffTo(0).Then() + highPassFilter.CutoffTo(0).Then() .CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic); }); @@ -124,7 +153,7 @@ namespace osu.Game.Tests.Visual.Audio AddStep("Filter Sweep (reverse)", () => { - highpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then() + highPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then() .CutoffTo(0, 2000, Easing.OutCubic); }); diff --git a/osu.Game/Audio/Effects/AudioFilter.cs b/osu.Game/Audio/Effects/AudioFilter.cs index 0152254945..d2a39e9db7 100644 --- a/osu.Game/Audio/Effects/AudioFilter.cs +++ b/osu.Game/Audio/Effects/AudioFilter.cs @@ -20,6 +20,8 @@ namespace osu.Game.Audio.Effects private readonly BQFParameters filter; private readonly BQFType type; + private bool isAttached; + private int cutoff; /// @@ -33,10 +35,8 @@ namespace osu.Game.Audio.Effects if (value == cutoff) return; - int oldValue = cutoff; cutoff = value; - - updateFilter(oldValue, cutoff); + updateFilter(cutoff); } } @@ -50,73 +50,58 @@ namespace osu.Game.Audio.Effects this.mixer = mixer; this.type = type; - switch (type) - { - case BQFType.HighPass: - cutoff = 1; - break; - - case BQFType.LowPass: - cutoff = MAX_LOWPASS_CUTOFF; - break; - - default: - cutoff = 500; // A default that should ensure audio remains audible for other filters. - break; - } - filter = new BQFParameters { lFilter = type, - fCenter = cutoff, fBandwidth = 0, - fQ = 0.7f // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0) + // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0) + fQ = 0.7f }; - // Don't start attached if this is low-pass or high-pass filter (as they have special auto-attach/detach logic) - if (type != BQFType.LowPass && type != BQFType.HighPass) - attachFilter(); + Cutoff = getInitialCutoff(type); } - private void attachFilter() + private int getInitialCutoff(BQFType type) { - Debug.Assert(!mixer.Effects.Contains(filter)); - mixer.Effects.Add(filter); - } - - private void detachFilter() - { - Debug.Assert(mixer.Effects.Contains(filter)); - mixer.Effects.Remove(filter); - } - - private void updateFilter(int oldValue, int newValue) - { - // Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz. - if (type == BQFType.LowPass) + switch (type) { - if (newValue >= MAX_LOWPASS_CUTOFF) - { - detachFilter(); - return; - } + case BQFType.HighPass: + return 1; - if (oldValue >= MAX_LOWPASS_CUTOFF) - attachFilter(); + case BQFType.LowPass: + return MAX_LOWPASS_CUTOFF; + + default: + return 500; // A default that should ensure audio remains audible for other filters. + } + } + + private void updateFilter(int newValue) + { + 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) + { + ensureDetached(); + return; + } + + break; + + // Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz. + case BQFType.HighPass: + if (newValue <= 1) + { + ensureDetached(); + return; + } + + break; } - // Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz. - if (type == BQFType.HighPass) - { - if (newValue <= 1) - { - detachFilter(); - return; - } - - if (oldValue <= 1) - attachFilter(); - } + ensureAttached(); var filterIndex = mixer.Effects.IndexOf(filter); @@ -131,12 +116,30 @@ namespace osu.Game.Audio.Effects } } + private void ensureAttached() + { + if (isAttached) + return; + + Debug.Assert(!mixer.Effects.Contains(filter)); + mixer.Effects.Add(filter); + isAttached = true; + } + + private void ensureDetached() + { + if (!isAttached) + return; + + Debug.Assert(mixer.Effects.Contains(filter)); + mixer.Effects.Remove(filter); + isAttached = false; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - - if (mixer.Effects.Contains(filter)) - detachFilter(); + ensureDetached(); } } } From e36c484060edeb764626b49a23b4ed45cf73ae11 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 13 Oct 2021 15:30:58 +0900 Subject: [PATCH 378/391] Ensure rewinding before the spinner's start time --- .../TestSceneSpinnerRotation.cs | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 9da583a073..52ab39cfbd 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneSpinnerRotation : TestSceneOsuPlayer { + private const double spinner_start_time = 100; + private const double spinner_duration = 6000; + [Resolved] private AudioManager audioManager { get; set; } @@ -77,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests double finalTrackerRotation = 0, trackerRotationTolerance = 0; double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0; - addSeekStep(5000); + addSeekStep(spinner_start_time + 5000); AddStep("retrieve disc rotation", () => { finalTrackerRotation = drawableSpinner.RotationTracker.Rotation; @@ -90,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation); - addSeekStep(2500); + addSeekStep(spinner_start_time + 2500); AddAssert("disc rotation rewound", // we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in. // due to the exponential damping applied we're allowing a larger margin of error of about 10% @@ -102,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Tests // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error. () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100)); - addSeekStep(5000); + addSeekStep(spinner_start_time + 5000); AddAssert("is disc rotation almost same", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance)); AddAssert("is symbol rotation almost same", @@ -140,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestSpinnerNormalBonusRewinding() { - addSeekStep(1000); + addSeekStep(spinner_start_time + 1000); AddAssert("player score matching expected bonus score", () => { @@ -201,24 +204,9 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0)); } - private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay - { - Frames = scoreReplay - .Frames - .Cast() - .Select(replayFrame => - { - var adjustedTime = replayFrame.Time * rate; - return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray()); - }) - .Cast() - .ToList() - }; - private void addSeekStep(double time) { AddStep($"seek to {time}", () => Player.GameplayClockContainer.Seek(time)); - AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); } @@ -241,7 +229,8 @@ namespace osu.Game.Rulesets.Osu.Tests new Spinner { Position = new Vector2(256, 192), - EndTime = 6000, + StartTime = spinner_start_time, + Duration = spinner_duration }, } }; From db5099de3ad38a6a986f4870ed30e4e97df51bfa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Oct 2021 15:45:01 +0900 Subject: [PATCH 379/391] Add missing licence header --- osu.Game.Tests/Database/GeneralUsageTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs index 245981cd9b..3e8b6091fd 100644 --- a/osu.Game.Tests/Database/GeneralUsageTests.cs +++ b/osu.Game.Tests/Database/GeneralUsageTests.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; using System.Threading; using System.Threading.Tasks; From 93d7cdc509c8ec384c85e82228345ca778d429f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Oct 2021 15:50:05 +0900 Subject: [PATCH 380/391] Don't check whether the source realm was closed or not Based on what we now know, this is not required, as long as there is another realm context open on the same thread. --- osu.Game/Database/RealmLive.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs index 71fb44f617..abb69644d6 100644 --- a/osu.Game/Database/RealmLive.cs +++ b/osu.Game/Database/RealmLive.cs @@ -102,7 +102,7 @@ namespace osu.Game.Database } } - private bool originalDataValid => isCorrectThread && data.IsValid && !data.Realm.IsClosed; + private bool originalDataValid => isCorrectThread && data.IsValid; // this matches realm's internal thread validation (see https://github.com/realm/realm-dotnet/blob/903b4d0b304f887e37e2d905384fb572a6496e70/Realm/Realm/Native/SynchronizationContextScheduler.cs#L72) private bool isCorrectThread From 5e934cdd2bdb4badf923c90fd8d66c0340f2f8a4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 13 Oct 2021 17:42:55 +0900 Subject: [PATCH 381/391] Make CFS error and fail the job --- .github/workflows/ci.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29cbdd2d37..128ae8f409 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,9 +79,12 @@ jobs: run: | # TODO: Add ignore filters and GitHub Workflow Command Reporting in CFS. That way we don't have to do this workaround. # FIXME: Suppress warnings from templates project - dotnet codefilesanity | while read -r line; do - echo "::warning::$line" - done + exit_code=0 + while read -r line; do + echo "::error::$line" + exit_code=1 + done <<< $(dotnet codefilesanity) + exit $exit_code # Temporarily disabled due to test failures, but it won't work anyway until the tool is upgraded. # - name: .NET Format (Dry Run) From e12249f1270a22cf5811a8bb7a9ee44f2c0250db Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 13 Oct 2021 17:56:33 +0900 Subject: [PATCH 382/391] Exclude empty lines --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 128ae8f409..0da1f9636b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,8 +81,10 @@ jobs: # FIXME: Suppress warnings from templates project exit_code=0 while read -r line; do + if [[ ! -z "$line" ]]; then echo "::error::$line" exit_code=1 + fi done <<< $(dotnet codefilesanity) exit $exit_code From 6d1b9be7fd4d5a482ac04dd9f01e0fe051bde2ff Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 13 Oct 2021 17:56:56 +0900 Subject: [PATCH 383/391] Test CFS failure --- osu.Game/OsuGame.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 020cdebab6..1c277a3bd7 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -1,6 +1,3 @@ -// 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; From 08bbdc70fc96a26832911db0a133c46ca289d2cf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 13 Oct 2021 17:57:09 +0900 Subject: [PATCH 384/391] Revert "Test CFS failure" This reverts commit 6d1b9be7fd4d5a482ac04dd9f01e0fe051bde2ff. --- osu.Game/OsuGame.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1c277a3bd7..020cdebab6 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.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; using System.Collections.Generic; using System.Diagnostics; From 2af1e6acc7bb0ffedda8fad659a215eed64ca3be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 13 Oct 2021 18:02:22 +0900 Subject: [PATCH 385/391] Update framework --- osu.Android.props | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 956093b2ac..db62667fc2 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 184c9d3f63..c6121ddd5f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index 38b920420b..110de79285 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From ceec81d54d8727986415a308686369453420b7ca Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 13 Oct 2021 20:26:54 +0900 Subject: [PATCH 386/391] Fix another spinner test failure --- osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs index 0ba775e5c7..37f1a846ad 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs @@ -45,8 +45,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods { new Spinner { - Duration = 2000, - Position = OsuPlayfield.BASE_SIZE / 2 + Duration = 6000, + Position = OsuPlayfield.BASE_SIZE / 2, } } }, From fdf714271117211dc4ed3897c307c2466203ad90 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 13 Oct 2021 21:24:54 +0900 Subject: [PATCH 387/391] Remove Room.Position --- osu.Game/Online/Rooms/Room.cs | 8 -------- osu.Game/Screens/OnlinePlay/Components/RoomManager.cs | 2 -- .../OnlinePlay/Lounge/Components/RoomsContainer.cs | 2 +- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 5f71b4be4a..39fc7f1da8 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -130,12 +130,6 @@ namespace osu.Game.Online.Rooms set => MaxAttempts.Value = value; } - /// - /// The position of this in the list. This is not read from or written to the API. - /// - [JsonIgnore] - public readonly Bindable Position = new Bindable(-1); // Todo: This does not need to exist. - public Room() { Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue)); @@ -192,8 +186,6 @@ namespace osu.Game.Online.Rooms RecentParticipants.Clear(); RecentParticipants.AddRange(other.RecentParticipants); } - - Position.Value = other.Position.Value; } public void RemoveExpiredPlaylistItems() diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 381849189d..97377278a6 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -116,8 +116,6 @@ namespace osu.Game.Screens.OnlinePlay.Components if (ignoredRooms.Contains(room.RoomID.Value.Value)) return; - room.Position.Value = -room.RoomID.Value.Value; - try { foreach (var pi in room.Playlist) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index 907b7e308a..85efdcef1a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -129,7 +129,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private void updateSorting() { foreach (var room in roomFlow) - roomFlow.SetLayoutPosition(room, room.Room.Position.Value); + roomFlow.SetLayoutPosition(room, -(room.Room.RoomID.Value ?? 0)); } protected override bool OnClick(ClickEvent e) From 47b6fb05f7cf0064f3cf900a048d142e4d4287da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Oct 2021 00:12:38 +0900 Subject: [PATCH 388/391] Add missing github sponsors funding spec --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 0c6b80e97e..fc61573416 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ +github: ppy custom: https://osu.ppy.sh/home/support From 200149c9d7203084a08d77303b8091a3634aea8d Mon Sep 17 00:00:00 2001 From: Xexxar Date: Wed, 13 Oct 2021 15:41:24 +0000 Subject: [PATCH 389/391] updated to newly refactored aim --- .../Preprocessing/OsuDifficultyHitObject.cs | 33 +++++-- .../Difficulty/Skills/Aim.cs | 98 ++++++++----------- osu.Game.Rulesets.Osu/Objects/Slider.cs | 6 ++ 3 files changed, 73 insertions(+), 64 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index b9ba6364af..89ba73ba51 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -22,11 +22,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing public double JumpDistance { get; private set; } /// - /// Normalized Vector from the end position of the previous to the start position of this . + /// Minimum distance from the end position of the previous to the start position of this . /// - public Vector2 JumpVector { get; private set; } + public double MovementDistance { get; private set; } - /// + /// JumpTravel /// Normalized distance between the start and end position of the previous . /// public double TravelDistance { get; private set; } @@ -37,6 +37,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing /// public double? Angle { get; private set; } + /// + /// Milliseconds elapsed since the end time of the Previous , with a minimum of 25ms. + /// + public double MovementTime { get; private set; } + + /// + /// Milliseconds elapsed since from the start time of the Previous to the end time of the same Previous , with a minimum of 25ms. + /// + public double TravelTime { get; private set; } + /// /// Milliseconds elapsed since the start time of the previous , with a minimum of 25ms. /// @@ -51,13 +61,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing this.lastLastObject = (OsuHitObject)lastLastObject; this.lastObject = (OsuHitObject)lastObject; - setDistances(); - // Capped to 25ms to prevent difficulty calculation breaking from simulatenous objects. StrainTime = Math.Max(DeltaTime, 25); + + setDistances(clockRate); } - private void setDistances() + private void setDistances(double clockRate) { // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps. float scalingFactor = normalized_radius / (float)BaseObject.Radius; @@ -72,6 +82,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing { computeSliderCursorPosition(lastSlider); TravelDistance = lastSlider.LazyTravelDistance * scalingFactor; + TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, 0); + MovementTime = Math.Max(StrainTime - TravelTime, 0); + MovementDistance = Math.Max(0, Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length - 0) * scalingFactor; } Vector2 lastCursorPosition = getEndCursorPosition(lastObject); @@ -79,8 +92,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing // Don't need to jump to reach spinners if (!(BaseObject is Spinner)) { - JumpVector = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor); - JumpDistance = JumpVector.Length; + JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length; + MovementDistance = Math.Min(JumpDistance, MovementDistance); } if (lastLastObject != null) @@ -104,7 +117,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing slider.LazyEndPosition = slider.StackedPosition; - float approxFollowCircleRadius = (float)(slider.Radius * 3); + float approxFollowCircleRadius = (float)(slider.Radius * 2.4); var computeVertex = new Action(t => { double progress = (t - slider.StartTime) / slider.SpanDuration; @@ -117,6 +130,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing var diff = slider.StackedPosition + slider.Path.PositionAt(progress) - slider.LazyEndPosition.Value; float dist = diff.Length; + slider.LazyTravelTime = t - slider.StartTime; + if (dist > approxFollowCircleRadius) { // The cursor would be outside the follow circle, we need to move it diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index 0101dcaee2..e162b25f10 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -7,7 +7,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; using osu.Framework.Utils; -using osuTK; namespace osu.Game.Rulesets.Osu.Difficulty.Skills { @@ -23,13 +22,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills protected override int HistoryLength => 2; - private const double wide_angle_multiplier = 1.0; - private const double acute_angle_multiplier = 1.0; - private const double rhythm_variance_multiplier = 1.0; + private const double wide_angle_multiplier = 1.5; + private const double acute_angle_multiplier = 2.0; private double currentStrain = 1; - private double skillMultiplier => 26.25; + private double skillMultiplier => 23.25; private double strainDecayBase => 0.15; private double strainValueOf(DifficultyHitObject current) @@ -41,75 +39,65 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills var osuPrevObj = (OsuDifficultyHitObject)Previous[0]; var osuLastObj = (OsuDifficultyHitObject)Previous[1]; - var currVector = Vector2.Divide(osuCurrObj.JumpVector, (float)osuCurrObj.StrainTime); - var prevVector = Vector2.Divide(osuPrevObj.JumpVector, (float)osuPrevObj.StrainTime); + double currVelocity = osuCurrObj.JumpDistance / osuCurrObj.StrainTime; // Start iwth the base distance / time - // Start with regular velocity. - double aimStrain = currVector.Length; - - if (Precision.AlmostEquals(osuCurrObj.StrainTime, osuPrevObj.StrainTime, 10)) // Rhythms are the same. + if (osuPrevObj.BaseObject is Slider) // If object is a slider { - if (osuCurrObj.Angle != null) + double movementVelocity = osuCurrObj.MovementDistance / osuCurrObj.MovementTime; // calculate the movement velocity from slider end to next note + double travelVelocity = osuCurrObj.TravelDistance / osuCurrObj.TravelTime; // calculate the slider velocity from slider head to lazy end. + + currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity. + } + + double prevVelocity = osuPrevObj.JumpDistance / osuPrevObj.StrainTime; // do the same for the previous velocity. + + if (osuLastObj.BaseObject is Slider) + { + double movementVelocity = osuPrevObj.MovementDistance / osuPrevObj.MovementTime; + double travelVelocity = osuPrevObj.TravelDistance / osuPrevObj.TravelTime; + + prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity); + } + + double angleBonus = 0; + + double aimStrain = currVelocity; // Start strain with regular velocity. + + if (Precision.AlmostEquals(osuCurrObj.StrainTime, osuPrevObj.StrainTime, 10)) // If rhythms are the same. + { + if (osuCurrObj.Angle != null && osuPrevObj.Angle != null) { - double angle = osuCurrObj.Angle.Value; + double currAngle = osuCurrObj.Angle.Value; + double prevAngle = osuPrevObj.Angle.Value; // Rewarding angles, take the smaller velocity as base. - double angleBonus = Math.Min(currVector.Length, prevVector.Length); + angleBonus = Math.Min(currVelocity, prevVelocity); - double wideAngleBonus = calcWideAngleBonus(angle); - double acuteAngleBonus = calcAcuteAngleBonus(angle); + double wideAngleBonus = calcWideAngleBonus(currAngle); + double acuteAngleBonus = calcAcuteAngleBonus(currAngle); - if (osuCurrObj.StrainTime > 100) + if (osuCurrObj.StrainTime > 100) // Only buff deltaTime exceeding 300 bpm 1/2. acuteAngleBonus = 0; else - { - acuteAngleBonus *= Math.Min(2, Math.Pow((100 - osuCurrObj.StrainTime) / 15, 1.5)); - wideAngleBonus *= Math.Pow(osuCurrObj.StrainTime / 100, 6); - } + acuteAngleBonus *= calcAcuteAngleBonus(prevAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern. + * Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime + * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4 + * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Min(100, osuCurrObj.JumpDistance) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter). - if (acuteAngleBonus > wideAngleBonus) - angleBonus = Math.Min(angleBonus, 150 / osuCurrObj.StrainTime) * Math.Min(1, Math.Pow(Math.Min(osuCurrObj.JumpDistance, osuPrevObj.JumpDistance) / 150, 2)); + wideAngleBonus *= angleBonus * (1 - Math.Pow(calcWideAngleBonus(prevAngle), 3)); // Penalize wide angles if they're repeated, reducing the penalty as the prevAngle gets more acute. - angleBonus *= Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier); - - // add in angle velocity. - aimStrain += angleBonus; + angleBonus = acuteAngleBonus * acute_angle_multiplier + wideAngleBonus * wide_angle_multiplier; // add the anglebuffs together. } } - else // There is a rhythm change - { - // Rewarding rhythm, take the smaller velocity as base. - double rhythmBonus = Math.Min(currVector.Length, prevVector.Length); - if (osuCurrObj.StrainTime + 10 < osuPrevObj.StrainTime && osuPrevObj.StrainTime > osuLastObj.StrainTime + 10) - // Don't want to reward for a rhythm change back to back (unless its a double, which is why this only checks for fast -> slow -> fast). - rhythmBonus = 0; - - aimStrain += rhythmBonus * rhythm_variance_multiplier; // add in rhythm velocity. - } + aimStrain += angleBonus; // Add in angle bonus. return aimStrain; } - private double calcWideAngleBonus(double angle) - { - if (angle < Math.PI / 3) - return 0; - if (angle < 2 * Math.PI / 3) - return Math.Pow(Math.Sin(1.5 * (angle - Math.PI / 3)), 2); + private double calcWideAngleBonus(double angle) => Math.Pow(Math.Sin(3.0 / 4 * (Math.Min(5.0 / 6 * Math.PI, Math.Max(Math.PI / 6, angle)) - Math.PI / 6)), 2); - return 0.25 + 0.75 * Math.Pow(Math.Sin(1.5 * (Math.PI - angle)), 2); - } - - private double calcAcuteAngleBonus(double angle) - { - if (angle < Math.PI / 3) - return 0.5 + 0.5 * Math.Pow(Math.Sin(1.5 * angle), 2); - if (angle < 2 * Math.PI / 3) - return Math.Pow(Math.Sin(1.5 * (2 * Math.PI / 3 - angle)), 2); - - return 0; - } + private double calcAcuteAngleBonus(double angle) => 1 - calcWideAngleBonus(angle); private double applyDiminishingExp(double val) => Math.Pow(val, 0.99); diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 1d2666f46b..1d494c2917 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -79,6 +79,12 @@ namespace osu.Game.Rulesets.Osu.Objects /// internal float LazyTravelDistance; + /// + /// The time taken by the cursor upon completion of this if it was hit + /// with as few movements as possible. This is set and used by difficulty calculation. + /// + internal double LazyTravelTime; + public List> NodeSamples { get; set; } = new List>(); [JsonIgnore] From 0f248d55107e574876d139b2d030f98575f62704 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Wed, 13 Oct 2021 15:45:42 +0000 Subject: [PATCH 390/391] removed the merge conflict markers --- .../OsuDifficultyCalculatorTest.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 1096e9a777..15675e74d1 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -15,15 +15,6 @@ namespace osu.Game.Rulesets.Osu.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; -<<<<<<< HEAD - [TestCase(6.4766744062777439d, "diffcalc-test")] - [TestCase(0.63190213974653298d, "zero-length-sliders")] - public void Test(double expected, string name) - => base.Test(expected, name); - - [TestCase(8.9926029916156764d, "diffcalc-test")] - [TestCase(0.78116445583636385d, "zero-length-sliders")] -======= [TestCase(6.5867229481955389d, "diffcalc-test")] [TestCase(1.0416315570967911d, "zero-length-sliders")] public void Test(double expected, string name) @@ -31,7 +22,6 @@ namespace osu.Game.Rulesets.Osu.Tests [TestCase(8.2730989071947896d, "diffcalc-test")] [TestCase(1.2726413186221039d, "zero-length-sliders")] ->>>>>>> ppy/master public void TestClockRateAdjusted(double expected, string name) => Test(expected, name, new OsuModDoubleTime()); From 0292fe1c834ebf68f22f85832a776b6740030865 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Wed, 13 Oct 2021 16:04:39 +0000 Subject: [PATCH 391/391] removed residual code leftover --- .../Difficulty/Preprocessing/OsuDifficultyHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index 89ba73ba51..c2a4dc741f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing TravelDistance = lastSlider.LazyTravelDistance * scalingFactor; TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, 0); MovementTime = Math.Max(StrainTime - TravelTime, 0); - MovementDistance = Math.Max(0, Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length - 0) * scalingFactor; + MovementDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor; } Vector2 lastCursorPosition = getEndCursorPosition(lastObject);